Skip to content

Commit

Permalink
4.x: Policy statement method matching added (#9501)
Browse files Browse the repository at this point in the history
Policy statement method matching added

Signed-off-by: David Kral <[email protected]>
  • Loading branch information
Verdent authored Dec 13, 2024
1 parent 1e423dd commit 7919684
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,5 @@ server:
endpoints:
- path: "/somePath"
config:
abac.policy-validator.statement: "${env.time.year >= 2017}"
abac.policy-validator.statement: "\\${env.time.year >= 2017}"
----
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ protected SecurityFilterContext initRequestFiltering(ContainerRequestContext req
.map(definitionMethod -> {
context.methodSecurity(getMethodSecurity(invokedResource,
definitionMethod,
(ExtendedUriInfo) requestContext.getUriInfo()));
(ExtendedUriInfo) requestContext.getUriInfo(),
requestContext));
context.resourceName(definitionMethod.getDeclaringClass().getSimpleName());

return configureContext(context, requestContext, requestContext.getUriInfo());
Expand Down Expand Up @@ -347,7 +348,8 @@ private SecurityDefinition securityForClass(Class<?> theClass, SecurityDefinitio

private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
Method definitionMethod,
ExtendedUriInfo uriInfo) {
ExtendedUriInfo uriInfo,
ContainerRequestContext requestContext) {
// Check cache

// Jersey model 'definition method' is the method that contains JAX-RS/Jersey annotations. JAX-RS does not support
Expand Down Expand Up @@ -414,7 +416,10 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
for (Method method : methodsToProcess) {
Class<?> clazz = method.getDeclaringClass();
current = securityForClass(clazz, current);
SecurityDefinition methodDef = processMethod(current.copyMe(), uriInfo.getPath(), method);
SecurityDefinition methodDef = processMethod(current.copyMe(),
uriInfo.getPath(),
requestContext.getMethod(),
method);

SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(methodDef.securityLevels().size() - 1);

Expand Down Expand Up @@ -453,7 +458,10 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource,
}

SecurityDefinition resClassSecurity = obtainClassSecurityDefinition(appRealClass, appClassSecurity, definitionClass);
SecurityDefinition methodDef = processMethod(resClassSecurity, uriInfo.getRequestUri().getPath(), definitionMethod);
SecurityDefinition methodDef = processMethod(resClassSecurity,
uriInfo.getRequestUri().getPath(),
requestContext.getMethod(),
definitionMethod);

int index = methodDef.securityLevels().size() - 1;
SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(index);
Expand Down Expand Up @@ -518,9 +526,9 @@ List<AnnotationAnalyzer> analyzers() {
return this.analyzers;
}

private SecurityDefinition processMethod(SecurityDefinition current, String path, Method method) {
private SecurityDefinition processMethod(SecurityDefinition current, String path, String httpMethod, Method method) {
SecurityDefinition methodDef = current.copyMe();
findMethodConfig(UriPath.create(path))
findMethodConfig(UriPath.create(path), httpMethod)
.asNode()
.ifPresentOrElse(methodDef::fromConfig,
() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
Expand Down Expand Up @@ -152,7 +154,7 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit

SecurityEnvironment env = envBuilder.build();
Map<String, Config> configMap = new HashMap<>();
findMethodConfig(UriPath.create(requestUri.getPath()))
findMethodConfig(UriPath.create(requestUri.getPath()), request.getMethod())
.asNode()
.ifPresent(conf -> conf.asNodeList().get().forEach(node -> configMap.put(node.name(), node)));

Expand Down Expand Up @@ -183,9 +185,10 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit
}
}

Config findMethodConfig(UriPath path) {
Config findMethodConfig(UriPath path, String method) {
return PATH_CONFIGS.get()
.stream()
.filter(pathConfig -> pathConfig.method.isEmpty() || pathConfig.method.contains(method.toUpperCase()))
.filter(pathConfig -> pathConfig.pathMatcher.prefixMatch(path).accepted())
.findFirst()
.map(PathConfig::config)
Expand Down Expand Up @@ -487,12 +490,15 @@ Config config(String child) {
return security.configFor(child);
}

private record PathConfig(PathMatcher pathMatcher, Config config) {
private record PathConfig(PathMatcher pathMatcher, Set<String> method, Config config) {

static PathConfig create(Config config) {
String path = config.get("path").asString().orElseThrow();
Set<String> method = config.get("method").asList(String.class)
.map(list -> list.stream().map(String::toUpperCase).collect(Collectors.toSet()))
.orElse(Set.of());
PathMatcher matcher = PathMatchers.create(path);
return new PathConfig(matcher, config.get("config"));
return new PathConfig(matcher, method, config.get("config"));
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.tests.integration.security.abac.policy;

import io.helidon.security.abac.policy.PolicyValidator;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

/**
* Policy statement matched by the method test resource.
*/
@Path("method")
public class PolicyStatementMethod {

/**
* Endpoint which should be matched via method.
*
* @return passed value
*/
@GET
@PolicyValidator.PolicyStatement("${env.time.year < 2017}")
public String get() {
return "passed";
}

/**
* Endpoint which should not be matched because of the different method.
*
* @return should not pass value
*/
@POST
@PolicyValidator.PolicyStatement("${env.time.year < 2017}")
public String post() {
return "should not pass";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ server:
endpoints:
- path: "/policy/override"
config:
abac.policy-validator.statement: "${env.time.year >= 2017}"
abac.policy-validator.statement: "\\${env.time.year >= 2017}"
- path: "/policy/asterisk*"
config:
abac.policy-validator.statement: "${env.time.year >= 2017}"
abac.policy-validator.statement: "\\${env.time.year >= 2017}"
- path: "/explicit/configuration"
config:
authorize: true
authorization-explicit: true
abac.policy-validator.statement: "${env.time.year >= 2017}"
abac.policy-validator.statement: "\\${env.time.year >= 2017}"
- path: "/method"
method: "GET"
config:
abac.policy-validator.statement: "\\${env.time.year >= 2017}"

security:
providers:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.tests.integration.security.abac.policy;

import io.helidon.http.Status;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@HelidonTest
public class MethodPolicyTest {

@Test
void testPolicyMatchedByMethod(WebTarget target) {
try (Response response = target.path("/method")
.request()
.get()) {
assertThat(response.getStatus(), is(Status.OK_200.code()));
assertThat(response.readEntity(String.class), is("passed"));
}
}


@Test
void testPolicyNotMatchedByMethod(WebTarget target) {
try (Response response = target.path("/method")
.request()
.post(Entity.text("Test value"))) {
assertThat(response.getStatus(), is(Status.FORBIDDEN_403.code()));
}
}

}

0 comments on commit 7919684

Please sign in to comment.