From c47168c34d84efb6f96feb442e36b6d9cdfdbcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 11 Nov 2023 23:39:50 +0100 Subject: [PATCH] Introduce runtime named HTTP Security Policies --- ...ity-authorize-web-endpoints-reference.adoc | 53 +++++++++++- .../HttpSecurityPolicyBuildItem.java | 5 ++ .../deployment/HttpSecurityProcessor.java | 13 ++- .../security/CustomNamedHttpSecPolicy.java | 29 +++++++ .../CustomNamedHttpSecPolicyTest.java | 82 +++++++++++++++++++ ...bstractPathMatchingHttpSecurityPolicy.java | 22 +++-- .../http/runtime/security/HttpAuthorizer.java | 8 +- .../runtime/security/HttpSecurityPolicy.java | 21 +++-- .../security/HttpSecurityRecorder.java | 22 +++-- ...agementPathMatchingHttpSecurityPolicy.java | 7 +- .../PathMatchingHttpSecurityPolicy.java | 18 +--- 11 files changed, 238 insertions(+), 42 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index d503365f9be9b..41a9d39bf3fed 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -68,6 +68,52 @@ It is an exact path match because it does not end with `*`. <3> This permission set references the previously defined policy. `roles1` is an example name; you can call the permission sets whatever you want. +=== Custom HttpSecurityPolicy + +Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI +bean that implements the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy` interface like in the example below: + +[source,java] +---- +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (customRequestAuthorization(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + @Override + public String name() { + return "custom"; <1> + } +} +---- +<1> Named HTTP Security policy will only be applied to requests matched by the `application.properties` path matching rules. + +.Example of custom named HttpSecurityPolicy referenced from configuration file +[source,properties] +---- +quarkus.http.auth.permission.custom1.paths=/custom/* +quarkus.http.auth.permission.custom1.policy=custom <1> +---- +<1> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method. + +[TIP] +==== +You can also create global `HttpSecurityPolicy` invoked on every request. +Just do not implement the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method and leave the policy nameless. +==== === Matching on paths and methods @@ -642,19 +688,22 @@ Similarly to the `CRUDResource` example, the following example shows how you can ---- package org.acme.library; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.security.Permission; import java.util.Arrays; import java.util.Set; +@RegisterForReflection <1> public class MediaLibraryPermission extends LibraryPermission { public MediaLibraryPermission(String libraryName, String[] actions) { - super(libraryName, actions, new MediaLibrary()); <1> + super(libraryName, actions, new MediaLibrary()); <2> } } ---- -<1> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. +<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter. +<2> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor. [source,properties] ---- diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java index cfa973812af24..eea7841fecdb9 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityPolicyBuildItem.java @@ -5,6 +5,11 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +/** + * @deprecated Define {@link io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy} CDI bean with {@link #name} + * set as the {@link HttpSecurityPolicy#name()}. + */ +@Deprecated public final class HttpSecurityPolicyBuildItem extends MultiBuildItem { final String name; diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index fb34fd418ef28..f248bb3428b5e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -33,6 +33,7 @@ import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; @@ -45,10 +46,18 @@ public class HttpSecurityProcessor { @Record(ExecutionTime.STATIC_INIT) @BuildStep void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + BuildProducer syntheticBeanProducer, HttpSecurityRecorder recorder) { if (!httpSecurityPolicyBuildItems.isEmpty()) { - recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect( - Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier))); + httpSecurityPolicyBuildItems.forEach(item -> syntheticBeanProducer + .produce(SyntheticBeanBuildItem + .configure(HttpSecurityPolicy.class) + .named(HttpSecurityPolicy.class.getName() + "." + item.getName()) + .runtimeValue(recorder.createNamedHttpSecurityPolicy(item.getPolicySupplier(), item.getName())) + .addType(HttpSecurityPolicy.class) + .scope(Singleton.class) + .unremovable() + .done())); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java new file mode 100644 index 0000000000000..61d8213fd9e2b --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicy.java @@ -0,0 +1,29 @@ +package io.quarkus.vertx.http.security; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy { + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if (isRequestAuthorized(request)) { + return Uni.createFrom().item(CheckResult.PERMIT); + } + return Uni.createFrom().item(CheckResult.DENY); + } + + private static boolean isRequestAuthorized(RoutingContext request) { + return request.request().headers().contains("hush-hush"); + } + + @Override + public String name() { + return "custom123"; + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java new file mode 100644 index 0000000000000..5e5aae1dfb2df --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomNamedHttpSecPolicyTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.security; + +import java.util.function.Supplier; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class CustomNamedHttpSecPolicyTest { + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.authenticated.paths=admin\n" + + "quarkus.http.auth.permission.authenticated.policy=custom123\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, AdminPathHandler.class, + CustomNamedHttpSecPolicy.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @Test + public void testAdminPath() { + RestAssured + .given() + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(401); + RestAssured + .given() + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo(":/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .header("hush-hush", "ignored") + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(Matchers.equalTo("test:/admin")); + RestAssured + .given() + .auth() + .preemptive() + .basic("test", "test") + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(403); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index fa82007a034a3..304688d56f51d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -14,6 +14,8 @@ import java.util.Set; import java.util.function.Function; +import jakarta.enterprise.inject.Instance; + import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; @@ -34,8 +36,8 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); AbstractPathMatchingHttpSecurityPolicy(Map permissions, - Map rolePolicy, String rootPath, Map namedBuildTimePolicies) { - init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath); + Map rolePolicy, String rootPath, Instance installedPolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath); } public String getAuthMechanismName(RoutingContext routingContext) { @@ -158,11 +160,21 @@ public List findPermissionCheckers(RoutingContext context) { } private static Map toNamedHttpSecPolicies(Map rolePolicies, - Map namedBuildTimePolicies) { + Instance installedPolicies) { Map namedPolicies = new HashMap<>(); - if (!namedBuildTimePolicies.isEmpty()) { - namedPolicies.putAll(namedBuildTimePolicies); + for (Instance.Handle handle : installedPolicies.handles()) { + if (handle.getBean().getBeanClass().getSuperclass() == AbstractPathMatchingHttpSecurityPolicy.class) { + continue; + } + var policy = handle.get(); + if (policy.name() != null) { + if (policy.name().isBlank()) { + throw new ConfigurationException("HTTP Security policy '" + policy + "' name must not be blank"); + } + namedPolicies.put(policy.name(), policy); + } } + for (Map.Entry e : rolePolicies.entrySet()) { PolicyConfig policyConfig = e.getValue(); if (policyConfig.permissions.isEmpty()) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 190a777488873..6f4529041ab3b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -23,10 +23,12 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer { } private static List toList(Instance installedPolicies) { - List policies = new ArrayList<>(); + List globalPolicies = new ArrayList<>(); for (HttpSecurityPolicy i : installedPolicies) { - policies.add(i); + if (i.name() == null) { + globalPolicies.add(i); + } } - return policies; + return globalPolicies; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java index 88a10a5da1932..f0c9062360111 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java @@ -8,18 +8,27 @@ /** * An HTTP Security policy, that controls which requests are allowed to proceed. - * - * There are two different ways these policies can be installed. The easiest is to just create a CDI bean, in which - * case the policy will be invoked on every request. - * - * Alternatively HttpSecurityPolicyBuildItem can be used to create a named policy. This policy can then be referenced - * in the application.properties path matching rules, which allows this policy to be applied to specific requests. + * CDI beans implementing this interface are invoked on every request unless they define {@link #name()}. + * The policy with {@link #name()} can then be referenced in the application.properties path matching rules, + * which allows this policy to be applied only to specific requests. */ public interface HttpSecurityPolicy { Uni checkPermission(RoutingContext request, Uni identity, AuthorizationRequestContext requestContext); + /** + * HTTP Security policy name referenced in the application.properties path matching rules, which allows this + * policy to be applied to specific requests. The name must not be blank. When the name is {@code null}, policy + * will be applied to every request. + * + * @return policy name + */ + default String name() { + // null == global policy + return null; + } + /** * The results of a permission check */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 40cc75234ddce..fe3b4ede94414 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -110,12 +110,22 @@ public EagerSecurityInterceptorStorage get() { }; } - public void setBuildTimeNamedPolicies(Map> buildTimeNamedPolicies) { - Map nameToPolicy = new HashMap<>(); - for (Map.Entry> nameToSupplier : buildTimeNamedPolicies.entrySet()) { - nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get()); - } - PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy); + public RuntimeValue createNamedHttpSecurityPolicy(Supplier policySupplier, + String name) { + return new RuntimeValue<>(new HttpSecurityPolicy() { + private final HttpSecurityPolicy delegate = policySupplier.get(); + + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + return delegate.checkPermission(request, identity, requestContext); + } + + @Override + public String name() { + return name; + } + }); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java index 4a22ac63fa04e..037c3ceed32bb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementPathMatchingHttpSecurityPolicy.java @@ -1,7 +1,6 @@ package io.quarkus.vertx.http.runtime.security; -import java.util.Map; - +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; import io.quarkus.runtime.Startup; @@ -18,8 +17,8 @@ public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, - ManagementInterfaceConfiguration runTimeConfig) { - super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of()); + ManagementInterfaceConfiguration runTimeConfig, Instance installedPolicies) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index c5624c4b6c7a4..b258e4fa0be0e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -1,8 +1,6 @@ package io.quarkus.vertx.http.runtime.security; -import java.util.HashMap; -import java.util.Map; - +import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; import io.quarkus.runtime.Startup; @@ -18,17 +16,9 @@ @Singleton public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy implements HttpSecurityPolicy { - // this map is planned for removal very soon as runtime named policies will make it obsolete - private static final Map HTTP_SECURITY_BUILD_TIME_POLICIES = new HashMap<>(); - - PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig) { - super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, - HTTP_SECURITY_BUILD_TIME_POLICIES); - } - - static synchronized void replaceNamedBuildTimePolicies(Map newPolicies) { - HTTP_SECURITY_BUILD_TIME_POLICIES.clear(); - HTTP_SECURITY_BUILD_TIME_POLICIES.putAll(newPolicies); + PathMatchingHttpSecurityPolicy(HttpConfiguration httpConfig, HttpBuildTimeConfig buildTimeConfig, + Instance installedPolicies) { + super(httpConfig.auth.permissions, httpConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies); } }