From 19ce35fb771e6a4d2aac5d9b3b1a011bfecfb591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Mon, 6 Nov 2023 19:39:22 +0100 Subject: [PATCH] Move HTTP Permissions and Roles policies to runtime --- .../deployment/HttpSecurityProcessor.java | 198 +++------------ .../ManagementInterfaceSecurityProcessor.java | 54 +--- .../vertx/http/runtime/AuthConfig.java | 13 - .../vertx/http/runtime/AuthRuntimeConfig.java | 25 ++ .../vertx/http/runtime/HttpConfiguration.java | 5 + .../vertx/http/runtime/PolicyConfig.java | 1 + .../management/ManagementAuthConfig.java | 15 -- .../ManagementInterfaceConfiguration.java | 5 + .../ManagementInterfaceSecurityRecorder.java | 33 +-- .../ManagementRuntimeAuthConfig.java | 27 ++ ...bstractPathMatchingHttpSecurityPolicy.java | 174 ++++++++++++- .../security/FormAuthenticationMechanism.java | 52 ++++ .../security/HttpSecurityRecorder.java | 234 ++---------------- ...agementPathMatchingHttpSecurityPolicy.java | 13 + .../PathMatchingHttpSecurityPolicy.java | 25 +- 15 files changed, 386 insertions(+), 488 deletions(-) create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java 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 84b2aaca206dc..fb34fd418ef28 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 @@ -1,28 +1,21 @@ package io.quarkus.vertx.http.deployment; import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED; +import static io.quarkus.arc.processor.DotNames.SINGLETON; -import java.security.Permission; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Singleton; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -30,185 +23,70 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.configuration.ConfigurationException; -import io.quarkus.security.StringPermission; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; 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; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; import io.quarkus.vertx.http.runtime.security.VertxBlockingSecurityExecutor; import io.vertx.core.http.ClientAuth; import io.vertx.ext.web.RoutingContext; public class HttpSecurityProcessor { - @BuildStep @Record(ExecutionTime.STATIC_INIT) - public void builtins(BuildProducer producer, - BuildProducer reflectiveClassProducer, - CombinedIndexBuildItem combinedIndexBuildItem, - HttpBuildTimeConfig buildTimeConfig, HttpSecurityRecorder recorder, - BuildProducer beanProducer) { - producer.produce(new HttpSecurityPolicyBuildItem("deny", new SupplierImpl<>(new DenySecurityPolicy()))); - producer.produce(new HttpSecurityPolicyBuildItem("permit", new SupplierImpl<>(new PermitSecurityPolicy()))); - producer.produce( - new HttpSecurityPolicyBuildItem("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy()))); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); - } - Map> permClassToCreator = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - PolicyConfig policyConfig = e.getValue(); - if (policyConfig.permissions.isEmpty()) { - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed)))); - } else { - // create HTTP Security policy that checks allowed roles and grants SecurityIdentity permissions to - // requests that this policy allows to proceed - var permissionCreator = permClassToCreator.computeIfAbsent(policyConfig.permissionClass, - new Function>() { - @Override - public BiFunction apply(String s) { - if (StringPermission.class.getName().equals(s)) { - return recorder.stringPermissionCreator(); - } - boolean constructorAcceptsActions = validateConstructor(combinedIndexBuildItem.getIndex(), - policyConfig.permissionClass); - return recorder.customPermissionCreator(s, constructorAcceptsActions); - } - }); - var policy = recorder.createRolesAllowedPolicy(policyConfig.rolesAllowed, policyConfig.permissions, - permissionCreator); - producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), policy)); - } - } - - if (!permClassToCreator.isEmpty()) { - // we need to register Permission classes for reflection as strictly speaking - // they might not exactly match classes defined via `PermissionsAllowed#permission` - var permissionClassesArr = permClassToCreator.keySet().toArray(new String[0]); - reflectiveClassProducer - .produce(ReflectiveClassBuildItem.builder(permissionClassesArr).constructors().fields().methods().build()); - } - } - - private static boolean validateConstructor(IndexView index, String permissionClass) { - ClassInfo classInfo = index.getClassByName(permissionClass); - - if (classInfo == null) { - throw new ConfigurationException(String.format("Permission class '%s' is missing", permissionClass)); - } - - // must have exactly one constructor - if (classInfo.constructors().size() != 1) { - throw new ConfigurationException( - String.format("Permission class '%s' must have exactly one constructor", permissionClass)); - } - MethodInfo constructor = classInfo.constructors().get(0); - - // first parameter must be permission name (String) - if (constructor.parametersCount() == 0 || !isString(constructor.parameterType(0))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", - permissionClass, String.class.getName())); - } - - // second parameter (actions) is optional - if (constructor.parametersCount() == 1) { - // permission constructor accepts just name, no actions - return false; - } - - if (constructor.parametersCount() == 2) { - if (!isStringArray(constructor.parameterType(1))) { - throw new ConfigurationException( - String.format("Permission class '%s' constructor second parameter must be '%s' array", permissionClass, - String.class.getName())); - } - return true; + @BuildStep + void produceNamedHttpSecurityPolicies(List httpSecurityPolicyBuildItems, + HttpSecurityRecorder recorder) { + if (!httpSecurityPolicyBuildItems.isEmpty()) { + recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect( + Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier))); } - - throw new ConfigurationException(String.format( - "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", - permissionClass)); - } - - private static boolean isStringArray(Type type) { - return type.kind() == Type.Kind.ARRAY && isString(type.asArrayType().constituent()); - } - - private static boolean isString(Type type) { - return type.kind() == Type.Kind.CLASS && type.name().toString().equals(String.class.getName()); } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initFormAuth( + @Record(ExecutionTime.STATIC_INIT) + AdditionalBeanBuildItem initFormAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, BuildProducer filterBuildItemBuildProducer) { - if (!buildTimeConfig.auth.proactive) { - filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) - .handler(recorder.formAuthPostHandler()).build()); - } if (buildTimeConfig.auth.form.enabled) { - return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupFormAuth()).done(); + if (!buildTimeConfig.auth.proactive) { + filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(buildTimeConfig.auth.form.postLocation) + .handler(recorder.formAuthPostHandler()).build()); + } + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(FormAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - SyntheticBeanBuildItem initMtlsClientAuth( - HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig) { + AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) { if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { - return SyntheticBeanBuildItem.configure(MtlsAuthenticationMechanism.class) - .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() - .scope(Singleton.class) - .supplier(recorder.setupMtlsClientAuth()).done(); + return AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(MtlsAuthenticationMechanism.class) + .setDefaultScope(SINGLETON).build(); } return null; } - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( HttpSecurityRecorder recorder, HttpBuildTimeConfig buildTimeConfig, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, BuildProducer securityInformationProducer) { - if (!applicationBasicAuthRequired(buildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth(buildTimeConfig)); if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig) @@ -247,18 +125,8 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, HttpBuildTimeConfig buildTimeConfig, - List httpSecurityPolicyBuildItemList, BuildProducer securityInformationProducer) { - Map> policyMap = new HashMap<>(); - for (HttpSecurityPolicyBuildItem e : httpSecurityPolicyBuildItemList) { - if (policyMap.containsKey(e.getName())) { - throw new RuntimeException("Multiple HTTP security policies defined with name " + e.getName()); - } - policyMap.put(e.getName(), e.policySupplier); - } - if (!buildTimeConfig.auth.form.enabled && buildTimeConfig.auth.basic.orElse(false)) { securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } @@ -270,21 +138,13 @@ void setupAuthenticationMechanisms( beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(HttpAuthenticator.class) .addBeanClass(HttpAuthorizer.class).build()); + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); filterBuildItemBuildProducer .produce(new FilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), FilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer .produce(new FilterBuildItem(recorder.permissionCheckHandler(), FilterBuildItem.AUTHORIZATION)); - - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } @@ -325,4 +185,18 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, private static boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) { return !ClientAuth.NONE.equals(buildTimeConfig.tlsClientAuth); } + + static class IsApplicationBasicAuthRequired implements BooleanSupplier { + private final boolean required; + + public IsApplicationBasicAuthRequired(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + required = applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig); + } + + @Override + public boolean getAsBoolean() { + return required; + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java index 07d36632f97ae..fc7d1e31a0b6b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ManagementInterfaceSecurityProcessor.java @@ -1,13 +1,8 @@ package io.quarkus.vertx.http.deployment; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - import jakarta.inject.Singleton; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -15,47 +10,26 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.IsApplicationBasicAuthRequired; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceSecurityRecorder; -import io.quarkus.vertx.http.runtime.security.AuthenticatedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; -import io.quarkus.vertx.http.runtime.security.SupplierImpl; public class ManagementInterfaceSecurityProcessor { - @BuildStep - public void builtins(ManagementInterfaceBuildTimeConfig buildTimeConfig, - BuildProducer beanProducer) { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(ManagementPathMatchingHttpSecurityPolicy.class)); - } - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIfNot = IsApplicationBasicAuthRequired.class) + @Record(ExecutionTime.STATIC_INIT) SyntheticBeanBuildItem initBasicAuth( - HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceSecurityRecorder recorder, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { - if (HttpSecurityProcessor.applicationBasicAuthRequired(httpBuildTimeConfig, managementInterfaceBuildTimeConfig)) { - return null; - } - if (managementInterfaceBuildTimeConfig.auth.basic.orElse(false)) { SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem .configure(BasicAuthenticationMechanism.class) .types(HttpAuthenticationMechanism.class) - .setRuntimeInit() .scope(Singleton.class) .supplier(recorder.setupBasicAuth()); return configurator.done(); @@ -71,39 +45,21 @@ void setupAuthenticationMechanisms( BuildProducer filterBuildItemBuildProducer, BuildProducer beanProducer, Capabilities capabilities, - BuildProducer beanContainerListenerBuildItemBuildProducer, ManagementInterfaceBuildTimeConfig buildTimeConfig) { - - Map> policyMap = new HashMap<>(); - for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { - policyMap.put(e.getKey(), - new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed))); - } - policyMap.put("deny", new SupplierImpl<>(new DenySecurityPolicy())); - policyMap.put("permit", new SupplierImpl<>(new PermitSecurityPolicy())); - policyMap.put("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy())); - if (buildTimeConfig.auth.basic.orElse(false) && capabilities.isPresent(Capability.SECURITY)) { beanProducer .produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClass(HttpAuthenticator.class) + .addBeanClass(ManagementPathMatchingHttpSecurityPolicy.class) .addBeanClass(ManagementInterfaceHttpAuthorizer.class).build()); filterBuildItemBuildProducer .produce(new ManagementInterfaceFilterBuildItem( recorder.authenticationMechanismHandler(buildTimeConfig.auth.proactive), ManagementInterfaceFilterBuildItem.AUTHENTICATION)); filterBuildItemBuildProducer - .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(buildTimeConfig, policyMap), + .produce(new ManagementInterfaceFilterBuildItem(recorder.permissionCheckHandler(), ManagementInterfaceFilterBuildItem.AUTHORIZATION)); - if (!buildTimeConfig.auth.permissions.isEmpty()) { - beanContainerListenerBuildItemBuildProducer - .produce(new BeanContainerListenerBuildItem(recorder.initPermissions(buildTimeConfig, policyMap))); - } - } else { - if (!buildTimeConfig.auth.permissions.isEmpty()) { - throw new IllegalStateException("HTTP permissions have been set however security is not enabled"); - } } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java index 3e609d66f9825..52b9017a38daa 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthConfig.java @@ -1,6 +1,5 @@ package io.quarkus.vertx.http.runtime; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -32,18 +31,6 @@ public class AuthConfig { @ConfigItem public Optional realm; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java new file mode 100644 index 0000000000000..eee0b3f84d897 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -0,0 +1,25 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Authentication mechanism information used for configuring HTTP auth instance for the deployment. + */ +@ConfigGroup +public class AuthRuntimeConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c19..e726692e1952b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -14,6 +14,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public class HttpConfiguration { + /** + * Authentication configuration + */ + public AuthRuntimeConfig auth; + /** * Enable the CORS filter. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java index 7d16bc2f4e392..6977b99f08770 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/PolicyConfig.java @@ -36,6 +36,7 @@ public class PolicyConfig { * Permissions granted by this policy will be created with a `java.security.Permission` implementation * specified by this configuration property. The permission class must declare exactly one constructor * that accepts permission name (`String`) or permission name and actions (`String`, `String[]`). + * Permission class must be registered for reflection if you run your application in a native mode. */ @ConfigItem(defaultValue = "io.quarkus.security.StringPermission") public String permissionClass = StringPermission.class.getName(); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java index 017fcfe953a66..a22db7e139359 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementAuthConfig.java @@ -1,12 +1,9 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.vertx.http.runtime.PolicyConfig; -import io.quarkus.vertx.http.runtime.PolicyMappingConfig; /** * Authentication for the management interface. @@ -20,18 +17,6 @@ public class ManagementAuthConfig { @ConfigItem public Optional basic; - /** - * The HTTP permissions - */ - @ConfigItem(name = "permission") - public Map permissions; - - /** - * The HTTP role based policies - */ - @ConfigItem(name = "policy") - public Map rolePolicy; - /** * If this is true and credentials are present then a user will always be authenticated * before the request progresses. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java index 9d77f458d1c90..7a2236a17f240 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -23,6 +23,11 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") public class ManagementInterfaceConfiguration { + /** + * Authentication configuration + */ + public ManagementRuntimeAuthConfig auth; + /** * The HTTP port */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java index 03e6be2d83742..f3b1b9f5a3c33 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceSecurityRecorder.java @@ -1,18 +1,13 @@ package io.quarkus.vertx.http.runtime.management; -import java.util.Map; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.security.AbstractPathMatchingHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.AbstractAuthenticationHandler; import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer; import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy; @@ -22,21 +17,11 @@ @Recorder public class ManagementInterfaceSecurityRecorder { - final RuntimeValue httpConfiguration; - final ManagementInterfaceBuildTimeConfig buildTimeConfig; - - public ManagementInterfaceSecurityRecorder(RuntimeValue httpConfiguration, - ManagementInterfaceBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } - public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new ManagementAuthenticationHandler(proactiveAuthentication); } - public Handler permissionCheckHandler(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { + public Handler permissionCheckHandler() { return new Handler() { volatile ManagementInterfaceHttpAuthorizer authorizer; @@ -52,17 +37,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(ManagementInterfaceBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(ManagementPathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - public Supplier setupBasicAuth() { return new Supplier() { @Override @@ -91,5 +65,10 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(ManagementInterfaceConfiguration.class).get().auth.permissions.isEmpty(); + } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java new file mode 100644 index 0000000000000..f9002d619a081 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.vertx.http.runtime.PolicyConfig; +import io.quarkus.vertx.http.runtime.PolicyMappingConfig; + +/** + * Authentication for the management interface. + */ +@ConfigGroup +public class ManagementRuntimeAuthConfig { + + /** + * The HTTP permissions + */ + @ConfigItem(name = "permission") + public Map permissions; + + /** + * The HTTP role based policies + */ + @ConfigItem(name = "policy") + public Map rolePolicy; +} 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 14aa4d607cd29..fa82007a034a3 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 @@ -1,6 +1,11 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; + +import java.lang.reflect.InvocationTargetException; +import java.security.Permission; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -8,9 +13,11 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.function.Supplier; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.PolicyConfig; import io.quarkus.vertx.http.runtime.PolicyMappingConfig; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; @@ -26,6 +33,11 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); + AbstractPathMatchingHttpSecurityPolicy(Map permissions, + Map rolePolicy, String rootPath, Map namedBuildTimePolicies) { + init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath); + } + public String getAuthMechanismName(RoutingContext routingContext) { PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { @@ -79,13 +91,8 @@ public Uni apply(CheckResult checkResult) { }); } - public void init(Map permissions, - Map> supplierMap, String rootPath) { - Map permissionCheckers = new HashMap<>(); - for (Map.Entry> i : supplierMap.entrySet()) { - permissionCheckers.put(i.getKey(), i.getValue().get()); - } - + private void init(Map permissions, + Map permissionCheckers, String rootPath) { Map> tempMap = new HashMap<>(); for (Map.Entry entry : permissions.entrySet()) { HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy); @@ -150,6 +157,157 @@ public List findPermissionCheckers(RoutingContext context) { } + private static Map toNamedHttpSecPolicies(Map rolePolicies, + Map namedBuildTimePolicies) { + Map namedPolicies = new HashMap<>(); + if (!namedBuildTimePolicies.isEmpty()) { + namedPolicies.putAll(namedBuildTimePolicies); + } + for (Map.Entry e : rolePolicies.entrySet()) { + PolicyConfig policyConfig = e.getValue(); + if (policyConfig.permissions.isEmpty()) { + namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed)); + } else { + final Map> roleToPermissions = new HashMap<>(); + for (Map.Entry> roleToPermissionStr : policyConfig.permissions.entrySet()) { + + // collect permission actions + // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 + Map cache = new HashMap<>(); + final String role = roleToPermissionStr.getKey(); + for (String permissionToAction : roleToPermissionStr.getValue()) { + // parse permission to actions and add it to cache + addPermissionToAction(cache, role, permissionToAction); + } + + // create permissions + var permissions = new HashSet(); + for (PermissionToActions helper : cache.values()) { + if (StringPermission.class.getName().equals(policyConfig.permissionClass)) { + permissions.add(new StringPermission(helper.permissionName, helper.actions.toArray(new String[0]))); + } else { + permissions.add(customPermissionCreator(policyConfig, helper)); + } + } + + roleToPermissions.put(role, Set.copyOf(permissions)); + } + namedPolicies.put(e.getKey(), + new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, Map.copyOf(roleToPermissions))); + } + } + namedPolicies.put("deny", new DenySecurityPolicy()); + namedPolicies.put("permit", new PermitSecurityPolicy()); + namedPolicies.put("authenticated", new AuthenticatedHttpSecurityPolicy()); + return namedPolicies; + } + + private static boolean acceptsActions(String permissionClassStr) { + var permissionClass = loadClass(permissionClassStr); + if (permissionClass.getConstructors().length != 1) { + throw new ConfigurationException( + String.format("Permission class '%s' must have exactly one constructor", permissionClass)); + } + var constructor = permissionClass.getConstructors()[0]; + // first parameter must be permission name (String) + if (constructor.getParameterCount() == 0 || !(constructor.getParameterTypes()[0] == String.class)) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor first parameter must be '%s' (permission name)", + permissionClass, String.class.getName())); + } + final boolean acceptsActions; + if (constructor.getParameterCount() == 1) { + acceptsActions = false; + } else { + if (constructor.getParameterCount() == 2) { + if (constructor.getParameterTypes()[1] != String[].class) { + throw new ConfigurationException( + String.format("Permission class '%s' constructor second parameter must be '%s' array", + permissionClass, + String.class.getName())); + } + } else { + throw new ConfigurationException(String.format( + "Permission class '%s' constructor must accept either one parameter (String permissionName), or two parameters (String permissionName, String[] actions)", + permissionClass)); + } + acceptsActions = true; + } + return acceptsActions; + } + + private static void addPermissionToAction(Map cache, String role, String permissionToAction) { + final String permissionName; + final String action; + // incoming value is either in format perm1:action1 or perm1 (with or withot action) + if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + // perm1:action1 + var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permToActions.length != 2) { + throw new ConfigurationException( + String.format("Invalid permission format '%s', please use exactly one permission to action separator", + permissionToAction)); + } + permissionName = permToActions[0].trim(); + action = permToActions[1].trim(); + } else { + // perm1 + permissionName = permissionToAction.trim(); + action = null; + } + + if (permissionName.isEmpty()) { + throw new ConfigurationException( + String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); + } + + cache.computeIfAbsent(permissionName, new Function() { + @Override + public PermissionToActions apply(String s) { + return new PermissionToActions(s); + } + }).addAction(action); + } + + private static Class loadClass(String className) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); + } + } + + private static Permission customPermissionCreator(PolicyConfig policyConfig, PermissionToActions helper) { + try { + var constructor = loadClass(policyConfig.permissionClass).getConstructors()[0]; + if (acceptsActions(policyConfig.permissionClass)) { + return (Permission) constructor.newInstance(helper.permissionName, helper.actions.toArray(new String[0])); + } else { + return (Permission) constructor.newInstance(helper.permissionName); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", + policyConfig.permissionClass, helper.permissionName, + Arrays.toString(helper.actions.toArray(new String[0]))), e); + } + } + + private static final class PermissionToActions { + private final String permissionName; + private final Set actions; + + private PermissionToActions(String permissionName) { + this.permissionName = permissionName; + this.actions = new HashSet<>(); + } + + private void addAction(String action) { + if (action != null) { + this.actions.add(action); + } + } + } + static class HttpMatcher { final String authMechanism; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 85927ed2f8922..48bfff2708aad 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -1,12 +1,16 @@ package io.quarkus.vertx.http.runtime.security; import java.net.URI; +import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import jakarta.inject.Inject; + import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; @@ -18,6 +22,9 @@ import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.Handler; @@ -47,6 +54,44 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final PersistentLoginManager loginManager; + //the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + + @Inject + FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { + String key; + if (!httpConfiguration.encryptionKey.isPresent()) { + if (encryptionKey != null) { + //persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + FormAuthConfig form = buildTimeConfig.auth.form; + this.loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), + form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), + form.cookiePath.orElse(null)); + this.loginPage = startWithSlash(form.loginPage.orElse(null)); + this.errorPage = startWithSlash(form.errorPage.orElse(null)); + this.landingPage = startWithSlash(form.landingPage.orElse(null)); + this.postLocation = startWithSlash(form.postLocation); + this.usernameParameter = form.usernameParameter; + this.passwordParameter = form.passwordParameter; + this.locationCookie = form.locationCookie; + this.cookiePath = form.cookiePath.orElse(null); + boolean redirectAfterLogin = form.redirectAfterLogin; + this.redirectToLandingPage = landingPage != null && redirectAfterLogin; + this.redirectToLoginPage = loginPage != null; + this.redirectToErrorPage = errorPage != null; + this.cookieSameSite = CookieSameSite.valueOf(form.cookieSameSite.name()); + } + public FormAuthenticationMechanism(String loginPage, String postLocation, String usernameParameter, String passwordParameter, String errorPage, String landingPage, boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath, @@ -240,4 +285,11 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM)); } + + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } } 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 84b3ef16064ad..40cc75234ddce 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 @@ -1,20 +1,9 @@ package io.quarkus.vertx.http.runtime.security; -import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR; - -import java.lang.reflect.InvocationTargetException; -import java.security.Permission; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -24,19 +13,14 @@ import org.jboss.logging.Logger; -import io.quarkus.arc.runtime.BeanContainer; -import io.quarkus.arc.runtime.BeanContainerListener; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; -import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; -import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; @@ -52,23 +36,6 @@ public class HttpSecurityRecorder { private static final Logger log = Logger.getLogger(HttpSecurityRecorder.class); - protected static final Consumer NOOP_CALLBACK = new Consumer() { - @Override - public void accept(Throwable throwable) { - - } - }; - - final RuntimeValue httpConfiguration; - final HttpBuildTimeConfig buildTimeConfig; - - //the temp encryption key, persistent across dev mode restarts - static volatile String encryptionKey; - - public HttpSecurityRecorder(RuntimeValue httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { - this.httpConfiguration = httpConfiguration; - this.buildTimeConfig = buildTimeConfig; - } public Handler authenticationMechanismHandler(boolean proactiveAuthentication) { return new HttpAuthenticationHandler(proactiveAuthentication); @@ -88,64 +55,6 @@ public void handle(RoutingContext event) { }; } - public BeanContainerListener initPermissions(HttpBuildTimeConfig buildTimeConfig, - Map> policies) { - return new BeanContainerListener() { - @Override - public void created(BeanContainer container) { - container.beanInstance(PathMatchingHttpSecurityPolicy.class) - .init(buildTimeConfig.auth.permissions, policies, buildTimeConfig.rootPath); - } - }; - } - - public Supplier setupFormAuth() { - - return new Supplier() { - - @Override - public FormAuthenticationMechanism get() { - String key; - if (!httpConfiguration.getValue().encryptionKey.isPresent()) { - if (encryptionKey != null) { - //persist across dev mode restarts - key = encryptionKey; - } else { - byte[] data = new byte[32]; - new SecureRandom().nextBytes(data); - key = encryptionKey = Base64.getEncoder().encodeToString(data); - log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key); - } - } else { - key = httpConfiguration.getValue().encryptionKey.get(); - } - FormAuthConfig form = buildTimeConfig.auth.form; - PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), - form.newCookieInterval.toMillis(), form.httpOnlyCookie, form.cookieSameSite.name(), - form.cookiePath.orElse(null)); - String loginPage = startWithSlash(form.loginPage.orElse(null)); - String errorPage = startWithSlash(form.errorPage.orElse(null)); - String landingPage = startWithSlash(form.landingPage.orElse(null)); - String postLocation = startWithSlash(form.postLocation); - String usernameParameter = form.usernameParameter; - String passwordParameter = form.passwordParameter; - String locationCookie = form.locationCookie; - String cookiePath = form.cookiePath.orElse(null); - boolean redirectAfterLogin = form.redirectAfterLogin; - return new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, - errorPage, landingPage, redirectAfterLogin, locationCookie, form.cookieSameSite.name(), cookiePath, - loginManager); - } - }; - } - - private static String startWithSlash(String page) { - if (page == null) { - return null; - } - return page.startsWith("/") ? page : "/" + page; - } - public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { return new Supplier() { @Override @@ -156,15 +65,6 @@ public BasicAuthenticationMechanism get() { }; } - public Supplier setupMtlsClientAuth() { - return new Supplier() { - @Override - public MtlsAuthenticationMechanism get() { - return new MtlsAuthenticationMechanism(); - } - }; - } - /** * This handler resolves the identity, and will be mapped to the post location. Otherwise, * for lazy auth the post will not be evaluated if there is no security rule for the post location. @@ -194,36 +94,6 @@ public void onFailure(Throwable throwable) { }; } - public BiFunction stringPermissionCreator() { - return StringPermission::new; - } - - public BiFunction customPermissionCreator(String clazz, boolean acceptsActions) { - return new BiFunction() { - @Override - public Permission apply(String name, String[] actions) { - try { - if (acceptsActions) { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name, actions); - } else { - return (Permission) loadClass(clazz).getConstructors()[0].newInstance(name); - } - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException( - String.format("Failed to create Permission - class '%s', name '%s', actions '%s'", clazz, - name, Arrays.toString(actions)), - e); - } - } - }; - } - - public Supplier createRolesAllowedPolicy(List rolesAllowed, - Map> roleToPermissionsStr, BiFunction permissionCreator) { - final Map> roleToPermissions = createPermissions(roleToPermissionsStr, permissionCreator); - return new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(rolesAllowed, roleToPermissions)); - } - public Supplier createSecurityInterceptorStorage( Map, Consumer> endpointRuntimeValToInterceptor) { @@ -240,63 +110,12 @@ public EagerSecurityInterceptorStorage get() { }; } - private static Map> createPermissions(Map> roleToPermissions, - BiFunction permissionCreator) { - // role -> created permissions - Map> result = new HashMap<>(); - for (Map.Entry> e : roleToPermissions.entrySet()) { - - // collect permission actions - // perm1:action1,perm2:action2,perm1:action3 -> perm1:action1,action3 and perm2:action2 - Map cache = new HashMap<>(); - final String role = e.getKey(); - for (String permissionToAction : e.getValue()) { - // parse permission to actions and add it to cache - addPermissionToAction(cache, role, permissionToAction); - } - - // create permissions - var permissions = new HashSet(); - for (PermissionToActions permission : cache.values()) { - permissions.add(permission.create(permissionCreator)); - } - - result.put(role, Set.copyOf(permissions)); + public void setBuildTimeNamedPolicies(Map> buildTimeNamedPolicies) { + Map nameToPolicy = new HashMap<>(); + for (Map.Entry> nameToSupplier : buildTimeNamedPolicies.entrySet()) { + nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get()); } - return Map.copyOf(result); - } - - private static void addPermissionToAction(Map cache, String role, String permissionToAction) { - final String permissionName; - final String action; - // incoming value is either in format perm1:action1 or perm1 (with or withot action) - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - // perm1:action1 - var permToActions = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permToActions.length != 2) { - throw new ConfigurationException( - String.format("Invalid permission format '%s', please use exactly one permission to action separator", - permissionToAction)); - } - permissionName = permToActions[0].trim(); - action = permToActions[1].trim(); - } else { - // perm1 - permissionName = permissionToAction.trim(); - action = null; - } - - if (permissionName.isEmpty()) { - throw new ConfigurationException( - String.format("Invalid permission name '%s' for role '%s'", permissionToAction, role)); - } - - cache.computeIfAbsent(permissionName, new Function() { - @Override - public PermissionToActions apply(String s) { - return new PermissionToActions(s); - } - }).addAction(action); + PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy); } public static abstract class DefaultAuthFailureHandler implements BiConsumer { @@ -380,10 +199,16 @@ protected void setPathMatchingPolicy(RoutingContext event) { event.put(AbstractPathMatchingHttpSecurityPolicy.class.getName(), pathMatchingPolicy); } } + + @Override + protected boolean httpPermissionsEmpty() { + return CDI.current().select(HttpConfiguration.class).get().auth.permissions.isEmpty(); + } } public static abstract class AbstractAuthenticationHandler implements Handler { volatile HttpAuthenticator authenticator; + volatile Boolean patchMatchingPolicyEnabled = null; final boolean proactiveAuthentication; public AbstractAuthenticationHandler(boolean proactiveAuthentication) { @@ -397,7 +222,12 @@ public void handle(RoutingContext event) { } //we put the authenticator into the routing context so it can be used by other systems event.put(HttpAuthenticator.class.getName(), authenticator); - setPathMatchingPolicy(event); + if (patchMatchingPolicyEnabled == null) { + setPatchMatchingPolicyEnabled(); + } + if (patchMatchingPolicyEnabled) { + setPathMatchingPolicy(event); + } //register the default auth failure handler if (proactiveAuthentication) { @@ -523,34 +353,14 @@ public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBool } } - protected abstract void setPathMatchingPolicy(RoutingContext event); - } - - private static final class PermissionToActions { - private final String permissionName; - private final Set actions; - - private PermissionToActions(String permissionName) { - this.permissionName = permissionName; - this.actions = new HashSet<>(); - } - - private void addAction(String action) { - if (action != null) { - this.actions.add(action); + private synchronized void setPatchMatchingPolicyEnabled() { + if (patchMatchingPolicyEnabled == null) { + patchMatchingPolicyEnabled = !httpPermissionsEmpty(); } } - private Permission create(BiFunction permissionCreator) { - return permissionCreator.apply(permissionName, actions.toArray(new String[0])); - } - } + protected abstract void setPathMatchingPolicy(RoutingContext event); - private static Class loadClass(String className) { - try { - return Thread.currentThread().getContextClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e); - } + protected abstract boolean httpPermissionsEmpty(); } } 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 fc0258841adf1..4a22ac63fa04e 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,12 +1,25 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy { + + ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration runTimeConfig) { + super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of()); + } + } 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 2132ab9532dd3..c5624c4b6c7a4 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,13 +1,34 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.HashMap; +import java.util.Map; + import jakarta.inject.Singleton; +import io.quarkus.runtime.Startup; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; + /** * A security policy that allows for matching of other security policies based on paths. * * This is used for the default path/method based RBAC. */ +@Startup // do not initialize path matcher during first HTTP request @Singleton -public class PathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy - implements HttpSecurityPolicy { +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); + } + }