Skip to content

Commit

Permalink
Runtime configuration for HTTP permissions, policy, form auth and realm
Browse files Browse the repository at this point in the history
closes quarkusio#19162 but mainly this is preparation for quarkusio#16728
  • Loading branch information
michalvavrik committed Mar 14, 2023
1 parent bc39d8a commit 94d4528
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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;
Expand All @@ -17,7 +16,6 @@
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.runtime.security.AuthenticatedHttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.DenySecurityPolicy;
Expand All @@ -30,7 +28,6 @@
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.vertx.core.http.ClientAuth;

Expand All @@ -43,14 +40,8 @@ public void builtins(BuildProducer<HttpSecurityPolicyBuildItem> producer, HttpBu
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));
}
for (Map.Entry<String, PolicyConfig> e : buildTimeConfig.auth.rolePolicy.entrySet()) {
producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(),
new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed))));
}

beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class));
}

@BuildStep
Expand All @@ -59,11 +50,11 @@ SyntheticBeanBuildItem 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) {
if (buildTimeConfig.auth.form) {
if (!buildTimeConfig.auth.proactive) {
filterBuildItemBuildProducer.produce(RouteBuildItem.builder().route(recorder.getFormPostLocation())
.handler(recorder.formAuthPostHandler()).build());
}
return SyntheticBeanBuildItem.configure(FormAuthenticationMechanism.class)
.types(HttpAuthenticationMechanism.class)
.setRuntimeInit()
Expand Down Expand Up @@ -99,7 +90,7 @@ SyntheticBeanBuildItem initBasicAuth(
return null;
}
boolean basicExplicitlyEnabled = buildTimeConfig.auth.basic.orElse(false);
if ((buildTimeConfig.auth.form.enabled || isMtlsClientAuthenticationEnabled(buildTimeConfig))
if ((buildTimeConfig.auth.form || isMtlsClientAuthenticationEnabled(buildTimeConfig))
&& !basicExplicitlyEnabled) {
//if form auth is enabled and we are not then we don't install
return null;
Expand All @@ -110,7 +101,7 @@ SyntheticBeanBuildItem initBasicAuth(
.setRuntimeInit()
.scope(Singleton.class)
.supplier(recorder.setupBasicAuth(buildTimeConfig));
if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig)
if (!buildTimeConfig.auth.form && !isMtlsClientAuthenticationEnabled(buildTimeConfig)
&& !basicExplicitlyEnabled) {
//if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined
configurator.defaultBean();
Expand All @@ -120,26 +111,35 @@ SyntheticBeanBuildItem initBasicAuth(
return configurator.done();
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void initPermissions(HttpSecurityRecorder recorder,
Capabilities capabilities,
List<HttpSecurityPolicyBuildItem> httpSecurityPolicyBuildItemList) {
if (capabilities.isPresent(Capability.SECURITY)) {
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);
}

recorder.initPermissions(policyMap);
}
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setupAuthenticationMechanisms(
HttpSecurityRecorder recorder,
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)) {
if (!buildTimeConfig.auth.form && buildTimeConfig.auth.basic.orElse(false)) {
securityInformationProducer.produce(SecurityInformationBuildItem.BASIC());
}

Expand All @@ -153,15 +153,6 @@ void setupAuthenticationMechanisms(
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.console.ConfiguredPathInfo;
import io.quarkus.vertx.http.runtime.BasicRoute;
import io.quarkus.vertx.http.runtime.HandlerType;
import io.quarkus.vertx.http.runtime.RouteCandidate;
import io.vertx.core.Handler;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
Expand Down Expand Up @@ -134,6 +136,21 @@ public Builder route(String route) {
return this;
}

/**
* Creates route if {@code routeCandidate} is resolved to the route path during runtime init.
* If {@code routeCandidate} supplies null, route is not going to be created.
* This way, extensions may create routes from runtime configuration properties.
*
* Only HTTP routes with {@link HandlerType#NORMAL} handler type and no {@code notFoundPage} are supported.
*
* @param routeCandidate route that may be resolved to null during runtime init
* @return Builder
*/
public Builder route(Supplier<String> routeCandidate) {
this.routeFunction = new RouteCandidate(routeCandidate);
return this;
}

/**
* @param route A normalized path used to define a basic route
* (e.g. use HttpRootPathBuildItem to construct/resolve the path value). This path this is also
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.vertx.http;

import java.util.function.Consumer;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.ConfigProvider;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildContext;
import io.quarkus.builder.BuildStep;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.restassured.RestAssured;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

public class RuntimeRouteCandidateTest {

private static final String APP_PROPS = "" +
"quarkus.http.root-path=/api\n" +
"route[1]=/build-time-route";

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset(APP_PROPS), "application.properties"))
.addBuildChainCustomizer(buildCustomizer())
.overrideRuntimeConfigKey("route[1]", "/runtime-route");

static Consumer<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@Override
public void accept(BuildChainBuilder builder) {
builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(RouteBuildItem.builder()
.route(new PathSupplier())
.handler(new MyHandler())
.build());
}
}).produces(RouteBuildItem.class)
.consumes(NonApplicationRootPathBuildItem.class)
.build();
}
};
}

public static class MyHandler implements Handler<RoutingContext> {
@Override
public void handle(RoutingContext routingContext) {
routingContext.response()
.setStatusCode(200)
.end(routingContext.request().path());
}
}

@Test
public void testRouteCreatedFromRuntimeProperty() {
RestAssured.given().get("/runtime-route").then().statusCode(200).body(Matchers.equalTo("/api/runtime-route"));
RestAssured.given().get("/build-time-route").then().statusCode(404);
}

public static class PathSupplier implements Supplier<String> {

@Override
public String get() {
return ConfigProvider.getConfig().getConfigValue("route[1]").getRawValue();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.vertx.http.runtime;

import java.util.Optional;

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 AuthBuildTimeConfig {

/**
* If basic auth should be enabled. If both basic and form auth is enabled then basic auth will be enabled in silent mode.
*
* If no authentication mechanisms are configured basic auth is the default.
*/
@ConfigItem
public Optional<Boolean> basic;

/**
* If form authentication is enabled.
*/
@ConfigItem(name = "form.enabled")
public boolean form;

/**
* If this is true and credentials are present then a user will always be authenticated
* before the request progresses.
*
* If this is false then an attempt will only be made to authenticate the user if a permission
* check is performed or the current user is required for some other reason.
*/
@ConfigItem(defaultValue = "true")
public boolean proactive;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@
*/
@ConfigGroup
public class AuthConfig {
/**
* If basic auth should be enabled. If both basic and form auth is enabled then basic auth will be enabled in silent mode.
*
* If no authentication mechanisms are configured basic auth is the default.
*/
@ConfigItem
public Optional<Boolean> basic;

/**
* Form Auth config
Expand All @@ -44,13 +37,4 @@ public class AuthConfig {
@ConfigItem(name = "policy")
public Map<String, PolicyConfig> rolePolicy;

/**
* If this is true and credentials are present then a user will always be authenticated
* before the request progresses.
*
* If this is false then an attempt will only be made to authenticate the user if a permission
* check is performed or the current user is required for some other reason.
*/
@ConfigItem(defaultValue = "true")
public boolean proactive;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ public enum CookieSameSite {
NONE
}

/**
* If form authentication is enabled.
*/
@ConfigItem
public boolean enabled;

/**
* The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class HttpBuildTimeConfig {
@ConvertWith(NormalizeRootHttpPathConverter.class)
public String rootPath;

public AuthConfig auth;
public AuthBuildTimeConfig auth;

/**
* Configures the engine to require/request client authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public class HttpConfiguration {

public AuthConfig auth;

/**
* Enable the CORS filter.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.vertx.http.runtime;

import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;

/**
* Creates {@link Function<Router, Route>} if {@link #path} underlying value
* is not null during runtime init.
*/
public class RouteCandidate implements Function<Router, Route> {

private Supplier<String> path;

public RouteCandidate() {
}

public RouteCandidate(Supplier<String> path) {
Objects.requireNonNull(path);
this.path = path;
}

public Supplier<String> getPath() {
return path;
}

public void setPath(Supplier<String> path) {
this.path = path;
}

/* RUNTIME_INIT */
@Override
public Route apply(Router router) {
final String resolvedPath = path.get();
if (resolvedPath == null) {
return null;
}
return router.route(resolvedPath);
}
}
Loading

0 comments on commit 94d4528

Please sign in to comment.