From e913e7639a1b9b570e9ab390e153c590e30494f2 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 14 Nov 2024 13:15:35 +0100 Subject: [PATCH] Policy statement method matching added Signed-off-by: David Kral --- .../includes/security/providers/abac.adoc | 2 +- .../microprofile/security/SecurityFilter.java | 20 ++++--- .../security/SecurityFilterCommon.java | 14 +++-- .../abac/policy/PolicyStatementMethod.java | 53 +++++++++++++++++++ .../src/main/resources/application.yaml | 10 ++-- .../abac/policy/MethodPolicyTest.java | 37 +++++++++++++ 6 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementMethod.java create mode 100644 tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/MethodPolicyTest.java diff --git a/docs/src/main/asciidoc/includes/security/providers/abac.adoc b/docs/src/main/asciidoc/includes/security/providers/abac.adoc index 1909d7dc145..235bc7ecb11 100644 --- a/docs/src/main/asciidoc/includes/security/providers/abac.adoc +++ b/docs/src/main/asciidoc/includes/security/providers/abac.adoc @@ -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}" ---- \ No newline at end of file diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java index 101a640e81f..1241b1d966e 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java @@ -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()); @@ -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 @@ -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); @@ -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); @@ -518,9 +526,9 @@ List 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, () -> { diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java index f37a97024ff..fa5527effd6 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java @@ -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; @@ -152,7 +154,7 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit SecurityEnvironment env = envBuilder.build(); Map 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))); @@ -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) @@ -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 method, Config config) { static PathConfig create(Config config) { String path = config.get("path").asString().orElseThrow(); + Set 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")); } } diff --git a/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementMethod.java b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementMethod.java new file mode 100644 index 00000000000..c89daae0318 --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementMethod.java @@ -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"; + } + +} diff --git a/tests/integration/security/abac-policy/src/main/resources/application.yaml b/tests/integration/security/abac-policy/src/main/resources/application.yaml index b3be701fd6a..bd65d56ad91 100644 --- a/tests/integration/security/abac-policy/src/main/resources/application.yaml +++ b/tests/integration/security/abac-policy/src/main/resources/application.yaml @@ -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: diff --git a/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/MethodPolicyTest.java b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/MethodPolicyTest.java new file mode 100644 index 00000000000..c852163ddfe --- /dev/null +++ b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/MethodPolicyTest.java @@ -0,0 +1,37 @@ +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())); + } + } + +}