Skip to content

Commit

Permalink
Move HTTP Security Policies and Permissions to runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Nov 5, 2023
1 parent fded7c0 commit e32e52e
Show file tree
Hide file tree
Showing 21 changed files with 575 additions and 488 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,37 @@ quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin
----
<1> This defines a role-based policy that allows users with the `user` and `admin` roles.

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<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> 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.

You can reference a custom policy by configuring the built-in permission sets that are defined in the `application.properties` file, as outlined in the following configuration example:

.Example of policy configuration
Expand All @@ -60,13 +91,17 @@ quarkus.http.auth.permission.deny1.policy=deny
quarkus.http.auth.permission.roles1.paths=/roles-secured/*,/other/*,/api/* <3>
quarkus.http.auth.permission.roles1.policy=role-policy1
quarkus.http.auth.permission.custom1.paths=/custom/*
quarkus.http.auth.permission.custom1.policy=custom <4>
----
<1> This permission references the default built-in `permit` policy to allow `GET` methods to `/public`.
In this case, the demonstrated setting would not affect this example because this request is allowed anyway.
<2> This permission references the built-in `deny` policy for `/forbidden`.
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.
<4> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method.


=== Matching on paths and methods
Expand Down Expand Up @@ -643,19 +678,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]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,61 +1,35 @@
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;
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.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<AdditionalBeanBuildItem> 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();
Expand All @@ -71,39 +45,21 @@ void setupAuthenticationMechanisms(
BuildProducer<ManagementInterfaceFilterBuildItem> filterBuildItemBuildProducer,
BuildProducer<AdditionalBeanBuildItem> beanProducer,
Capabilities capabilities,
BuildProducer<BeanContainerListenerBuildItem> beanContainerListenerBuildItemBuildProducer,
ManagementInterfaceBuildTimeConfig buildTimeConfig) {

Map<String, Supplier<HttpSecurityPolicy>> policyMap = new HashMap<>();
for (Map.Entry<String, PolicyConfig> 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");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> 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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 = "" +
"# Add your application.properties here, if applicable.\n" +
"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);
}

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

import java.util.Map;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
Expand Down Expand Up @@ -32,18 +31,6 @@ public class AuthConfig {
@ConfigItem
public Optional<String> realm;

/**
* The HTTP permissions
*/
@ConfigItem(name = "permission")
public Map<String, PolicyMappingConfig> permissions;

/**
* The HTTP role based policies
*/
@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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, PolicyMappingConfig> permissions;

/**
* The HTTP role based policies
*/
@ConfigItem(name = "policy")
public Map<String, PolicyConfig> rolePolicy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public class HttpConfiguration {

/**
* Authentication configuration
*/
public AuthRuntimeConfig auth;

/**
* Enable the CORS filter.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit e32e52e

Please sign in to comment.