Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move HTTP Permissions and Roles policies from build-time to runtime #36874

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,214 +1,92 @@
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;
import io.quarkus.deployment.annotations.BuildProducer;
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<HttpSecurityPolicyBuildItem> producer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
CombinedIndexBuildItem combinedIndexBuildItem,
HttpBuildTimeConfig buildTimeConfig, HttpSecurityRecorder recorder,
BuildProducer<AdditionalBeanBuildItem> 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<String, BiFunction<String, String[], Permission>> permClassToCreator = new HashMap<>();
for (Map.Entry<String, PolicyConfig> 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<String, BiFunction<String, String[], Permission>>() {
@Override
public BiFunction<String, String[], Permission> 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<HttpSecurityPolicyBuildItem> 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<RouteBuildItem> 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<SecurityInformationBuildItem> 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)
Expand Down Expand Up @@ -247,18 +125,8 @@ void setupAuthenticationMechanisms(
BuildProducer<FilterBuildItem> filterBuildItemBuildProducer,
BuildProducer<AdditionalBeanBuildItem> beanProducer,
Capabilities capabilities,
BuildProducer<BeanContainerListenerBuildItem> beanContainerListenerBuildItemBuildProducer,
HttpBuildTimeConfig buildTimeConfig,
List<HttpSecurityPolicyBuildItem> httpSecurityPolicyBuildItemList,
BuildProducer<SecurityInformationBuildItem> securityInformationProducer) {
Map<String, Supplier<HttpSecurityPolicy>> 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());
}
Expand All @@ -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");
}
}
}

Expand Down Expand Up @@ -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;
}
}
}
Loading
Loading