Skip to content

Commit

Permalink
Introduce runtime named HTTP Security Policies
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Nov 11, 2023
1 parent 2f2ce09 commit c47168c
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,52 @@ It is an exact path match because it does not end with `*`.
<3> This permission set references the previously defined policy.
`roles1` is an example name; you can call the permission sets whatever you want.

=== Custom HttpSecurityPolicy

Check warning on line 71 in docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Custom HttpSecurityPolicy'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Custom HttpSecurityPolicy'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 71, "column": 5}}}, "severity": "INFO"}

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.

.Example of custom named HttpSecurityPolicy referenced from configuration file
[source,properties]
----
quarkus.http.auth.permission.custom1.paths=/custom/*
quarkus.http.auth.permission.custom1.policy=custom <1>
----
<1> Custom policy name must match the value returned by the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method.

[TIP]
====
You can also create global `HttpSecurityPolicy` invoked on every request.
Just do not implement the `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name` method and leave the policy nameless.
====

=== Matching on paths and methods

Expand Down Expand Up @@ -642,19 +688,22 @@ Similarly to the `CRUDResource` example, the following example shows how you can
----
package org.acme.library;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.security.Permission;
import java.util.Arrays;
import java.util.Set;
@RegisterForReflection <1>
public class MediaLibraryPermission extends LibraryPermission {
public MediaLibraryPermission(String libraryName, String[] actions) {
super(libraryName, actions, new MediaLibrary()); <1>
super(libraryName, actions, new MediaLibrary()); <2>
}
}
----
<1> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor.
<1> When building a native executable, the permission class must be registered for reflection unless it is also used in at least one `io.quarkus.security.PermissionsAllowed#name` parameter.
<2> We want to pass the `MediaLibrary` instance to the `LibraryPermission` constructor.

[source,properties]
----
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.HttpAuthorizer;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder;
import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy;
Expand All @@ -45,10 +46,18 @@ public class HttpSecurityProcessor {
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void produceNamedHttpSecurityPolicies(List<HttpSecurityPolicyBuildItem> httpSecurityPolicyBuildItems,
BuildProducer<SyntheticBeanBuildItem> syntheticBeanProducer,
HttpSecurityRecorder recorder) {
if (!httpSecurityPolicyBuildItems.isEmpty()) {
recorder.setBuildTimeNamedPolicies(httpSecurityPolicyBuildItems.stream().collect(
Collectors.toMap(HttpSecurityPolicyBuildItem::getName, HttpSecurityPolicyBuildItem::getPolicySupplier)));
httpSecurityPolicyBuildItems.forEach(item -> syntheticBeanProducer
.produce(SyntheticBeanBuildItem
.configure(HttpSecurityPolicy.class)
.named(HttpSecurityPolicy.class.getName() + "." + item.getName())
.runtimeValue(recorder.createNamedHttpSecurityPolicy(item.getPolicySupplier(), item.getName()))
.addType(HttpSecurityPolicy.class)
.scope(Singleton.class)
.unremovable()
.done()));
}
}

Expand Down
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,82 @@
package io.quarkus.vertx.http.security;

import java.util.function.Supplier;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class CustomNamedHttpSecPolicyTest {

@BeforeAll
public static void setup() {
TestIdentityController.resetRoles().add("test", "test", "test");
}

private static final String APP_PROPS = "" +
"quarkus.http.auth.permission.authenticated.paths=admin\n" +
"quarkus.http.auth.permission.authenticated.policy=custom123\n";

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(TestIdentityController.class, TestIdentityProvider.class, AdminPathHandler.class,
CustomNamedHttpSecPolicy.class)
.addAsResource(new StringAsset(APP_PROPS), "application.properties");
}
});

@Test
public void testAdminPath() {
RestAssured
.given()
.when()
.get("/admin")
.then()
.assertThat()
.statusCode(401);
RestAssured
.given()
.when()
.header("hush-hush", "ignored")
.get("/admin")
.then()
.assertThat()
.statusCode(200)
.body(Matchers.equalTo(":/admin"));
RestAssured
.given()
.auth()
.preemptive()
.basic("test", "test")
.when()
.header("hush-hush", "ignored")
.get("/admin")
.then()
.assertThat()
.statusCode(200)
.body(Matchers.equalTo("test:/admin"));
RestAssured
.given()
.auth()
.preemptive()
.basic("test", "test")
.when()
.get("/admin")
.then()
.assertThat()
.statusCode(403);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.Set;
import java.util.function.Function;

import jakarta.enterprise.inject.Instance;

import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.StringPermission;
import io.quarkus.security.identity.SecurityIdentity;
Expand All @@ -34,8 +36,8 @@ public class AbstractPathMatchingHttpSecurityPolicy {
private final PathMatcher<List<HttpMatcher>> pathMatcher = new PathMatcher<>();

AbstractPathMatchingHttpSecurityPolicy(Map<String, PolicyMappingConfig> permissions,
Map<String, PolicyConfig> rolePolicy, String rootPath, Map<String, HttpSecurityPolicy> namedBuildTimePolicies) {
init(permissions, toNamedHttpSecPolicies(rolePolicy, namedBuildTimePolicies), rootPath);
Map<String, PolicyConfig> rolePolicy, String rootPath, Instance<HttpSecurityPolicy> installedPolicies) {
init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath);
}

public String getAuthMechanismName(RoutingContext routingContext) {
Expand Down Expand Up @@ -158,11 +160,21 @@ public List<HttpSecurityPolicy> findPermissionCheckers(RoutingContext context) {
}

private static Map<String, HttpSecurityPolicy> toNamedHttpSecPolicies(Map<String, PolicyConfig> rolePolicies,
Map<String, HttpSecurityPolicy> namedBuildTimePolicies) {
Instance<HttpSecurityPolicy> installedPolicies) {
Map<String, HttpSecurityPolicy> namedPolicies = new HashMap<>();
if (!namedBuildTimePolicies.isEmpty()) {
namedPolicies.putAll(namedBuildTimePolicies);
for (Instance.Handle<HttpSecurityPolicy> handle : installedPolicies.handles()) {
if (handle.getBean().getBeanClass().getSuperclass() == AbstractPathMatchingHttpSecurityPolicy.class) {
continue;
}
var policy = handle.get();
if (policy.name() != null) {
if (policy.name().isBlank()) {
throw new ConfigurationException("HTTP Security policy '" + policy + "' name must not be blank");
}
namedPolicies.put(policy.name(), policy);
}
}

for (Map.Entry<String, PolicyConfig> e : rolePolicies.entrySet()) {
PolicyConfig policyConfig = e.getValue();
if (policyConfig.permissions.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer {
}

private static List<HttpSecurityPolicy> toList(Instance<HttpSecurityPolicy> installedPolicies) {
List<HttpSecurityPolicy> policies = new ArrayList<>();
List<HttpSecurityPolicy> globalPolicies = new ArrayList<>();
for (HttpSecurityPolicy i : installedPolicies) {
policies.add(i);
if (i.name() == null) {
globalPolicies.add(i);
}
}
return policies;
return globalPolicies;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,27 @@

/**
* An HTTP Security policy, that controls which requests are allowed to proceed.
*
* There are two different ways these policies can be installed. The easiest is to just create a CDI bean, in which
* case the policy will be invoked on every request.
*
* Alternatively HttpSecurityPolicyBuildItem can be used to create a named policy. This policy can then be referenced
* in the application.properties path matching rules, which allows this policy to be applied to specific requests.
* CDI beans implementing this interface are invoked on every request unless they define {@link #name()}.
* The policy with {@link #name()} can then be referenced in the application.properties path matching rules,
* which allows this policy to be applied only to specific requests.
*/
public interface HttpSecurityPolicy {

Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> identity,
AuthorizationRequestContext requestContext);

/**
* HTTP Security policy name referenced in the application.properties path matching rules, which allows this
* policy to be applied to specific requests. The name must not be blank. When the name is {@code null}, policy
* will be applied to every request.
*
* @return policy name
*/
default String name() {
// null == global policy
return null;
}

/**
* The results of a permission check
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,22 @@ public EagerSecurityInterceptorStorage get() {
};
}

public void setBuildTimeNamedPolicies(Map<String, Supplier<HttpSecurityPolicy>> buildTimeNamedPolicies) {
Map<String, HttpSecurityPolicy> nameToPolicy = new HashMap<>();
for (Map.Entry<String, Supplier<HttpSecurityPolicy>> nameToSupplier : buildTimeNamedPolicies.entrySet()) {
nameToPolicy.put(nameToSupplier.getKey(), nameToSupplier.getValue().get());
}
PathMatchingHttpSecurityPolicy.replaceNamedBuildTimePolicies(nameToPolicy);
public RuntimeValue<HttpSecurityPolicy> createNamedHttpSecurityPolicy(Supplier<HttpSecurityPolicy> policySupplier,
String name) {
return new RuntimeValue<>(new HttpSecurityPolicy() {
private final HttpSecurityPolicy delegate = policySupplier.get();

@Override
public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> identity,
AuthorizationRequestContext requestContext) {
return delegate.checkPermission(request, identity, requestContext);
}

@Override
public String name() {
return name;
}
});
}

public static abstract class DefaultAuthFailureHandler implements BiConsumer<RoutingContext, Throwable> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.quarkus.vertx.http.runtime.security;

import java.util.Map;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;

import io.quarkus.runtime.Startup;
Expand All @@ -18,8 +17,8 @@
public class ManagementPathMatchingHttpSecurityPolicy extends AbstractPathMatchingHttpSecurityPolicy {

ManagementPathMatchingHttpSecurityPolicy(ManagementInterfaceBuildTimeConfig buildTimeConfig,
ManagementInterfaceConfiguration runTimeConfig) {
super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, Map.of());
ManagementInterfaceConfiguration runTimeConfig, Instance<HttpSecurityPolicy> installedPolicies) {
super(runTimeConfig.auth.permissions, runTimeConfig.auth.rolePolicy, buildTimeConfig.rootPath, installedPolicies);
}

}
Loading

0 comments on commit c47168c

Please sign in to comment.