Skip to content

Commit 68ccfbe

Browse files
committed
feat(security): add fluent-api for HTTP perms
1 parent 4435c0d commit 68ccfbe

19 files changed

+1496
-115
lines changed

docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ The context can be activated by users, for example with the `@ActivateRequestCon
190190
We recommend to let Quarkus activate and prepare CDI request context for you.
191191
For example, consider a situation where you want to inject a bean from the Jakarta REST context, such as the `jakarta.ws.rs.core.UriInfo` bean.
192192
In this case, you must apply the `HttpSecurityPolicy` to Jakarta REST endpoints. This can be achieved in one of the following ways:
193+
193194
* Use the `@AuthorizationPolicy` security annotation.
194195
* Set the `quarkus.http.auth.permission.custom1.applies-to=jaxrs` configuration property.
195196

@@ -445,6 +446,23 @@ quarkus.http.auth.roles-mapping.admin=Admin1 <1> <2>
445446
<1> Map the `admin` role to `Admin1` role. The `SecurityIdentity` will have both `admin` and `Admin1` roles.
446447
<2> The `/*` path is not secured. You must secure your endpoints with standard security annotations or define HTTP permissions in addition to this configuration property.
447448

449+
If you prefer a programmatic configuration, the same mapping can be added with the `io.quarkus.vertx.http.security.HttpSecurity` CDI event:
450+
451+
[source,java]
452+
----
453+
package org.acme.http.security;
454+
455+
import io.quarkus.vertx.http.security.HttpSecurity;
456+
import jakarta.enterprise.event.Observes;
457+
458+
public class HttpSecurityConfiguration {
459+
460+
void configure(@Observes HttpSecurity httpSecurity) {
461+
httpSecurity.rolesMapping("admin", "Admin1");
462+
}
463+
}
464+
----
465+
448466
=== Shared permission checks
449467

450468
One important rule for unshared permission checks is that only one path match is applied, the most specific one.
@@ -488,6 +506,95 @@ quarkus.http.auth.permission.roles3.policy=role-policy3
488506
<2> The `/secured/*` path can only be accessed by authenticated users. This way, you have secured the `/secured/all` path and so on.
489507
<3> Shared permissions are always applied before unshared ones, therefore a `SecurityIdentity` with the `root` role will have the `user` role as well.
490508

509+
=== Set up path-specific authorization programmatically
510+
511+
You can also configure the authorization policies presented by this guide so far programmatically.
512+
Consider the example mentioned earlier:
513+
514+
[source,properties]
515+
----
516+
quarkus.http.auth.permission.permit1.paths=/public/*
517+
quarkus.http.auth.permission.permit1.policy=permit
518+
quarkus.http.auth.permission.permit1.methods=GET
519+
520+
quarkus.http.auth.permission.deny1.paths=/forbidden
521+
quarkus.http.auth.permission.deny1.policy=deny
522+
523+
quarkus.http.auth.permission.roles1.paths=/roles-secured/*,/other/*,/api/*
524+
quarkus.http.auth.permission.roles1.policy=role-policy1
525+
quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin
526+
----
527+
528+
The same authorization policies can be configured programmatically:
529+
530+
[source,java]
531+
----
532+
package org.acme.http.security;
533+
534+
import jakarta.enterprise.event.Observes;
535+
536+
import io.quarkus.vertx.http.security.HttpSecurity;
537+
538+
public class HttpSecurityConfiguration {
539+
540+
void configure(@Observes HttpSecurity httpSecurity) {
541+
httpSecurity
542+
.path("/public/*").httpMethods("GET").authorization().permit()
543+
.path("/forbidden").authorization().deny()
544+
.path("/roles-secured/*", "/other/*", "/api/*").authorization().roles("admin", "user");
545+
}
546+
547+
}
548+
----
549+
550+
Additionally, the `io.quarkus.vertx.http.security.HttpSecurity` CDI event can be used to configure specific authentication mechanisms and policies:
551+
552+
[source,java]
553+
----
554+
package org.acme.http.security;
555+
556+
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
557+
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
558+
import io.quarkus.vertx.http.security.HttpSecurity;
559+
import jakarta.enterprise.event.Observes;
560+
import org.eclipse.microprofile.config.inject.ConfigProperty;
561+
562+
public class HttpSecurityConfiguration {
563+
564+
void configure(@Observes HttpSecurity httpSecurity, CustomHttpSecurityPolicy customHttpSecurityPolicy,
565+
@ConfigProperty(name = "secured-path") String securedPath) {
566+
567+
httpSecurity.path("/api/*").authenticationMechanism(new CustomAuthenticationMechanism())
568+
.authorization().authenticated(); <1>
569+
570+
httpSecurity.path("/other/*").basic().authorization().policy(customHttpSecurityPolicy); <2>
571+
572+
httpSecurity.path("/roles-secured/*").bearer().authorization()
573+
.policy(identity -> identity.hasRole("user") || "root".equals(identity.getPrincipal().getName())); <3>
574+
575+
httpSecurity.path("/other/administration").authorizationCodeFlow()
576+
.authorization().policy((identity, routingContext) -> {
577+
if (!identity.isAnonymous()) {
578+
String customAuthorization = routingContext.request().getHeader("Custom Authorization");
579+
return yourCustomAuthorizationCheck(customAuthorization);
580+
}
581+
return false;
582+
}); <4>
583+
584+
httpSecurity.path(securedPath).form().authorization().authenticated(); <5>
585+
586+
httpSecurity.path("/user-info").bearer().authorization().permissions("openid", "email", "profile"); <6>
587+
}
588+
}
589+
----
590+
<1> Authenticate all the '/api/' sub-paths with your own `HttpAuthenticationMechanism` instance.
591+
<2> Use the Basic authentication and authorize the requests with a custom `io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy`.
592+
<3> Use the Bearer token authentication and authorize the `SecurityIdentity` with your own policy.
593+
<4> Use Authorization Code Flow mechanism and write your own policy based on incoming request headers.
594+
<5> When Quarkus fires the `HttpSecurity` CDI event, the runtime configuration is ready.
595+
<6> Require that all the requests to the `/user-info` path have string permissions `openid`, `email` and `profile`.
596+
The same authorization can be required with the `@PermissionsAllowed(value = { "openid", "email", "profile" }, inclusive = true)` annotation instance placed on an endpoint.
597+
491598
[[standard-security-annotations]]
492599
== Authorization using annotations
493600

extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ImplicitBasicAuthAndBearerAuthCombinationTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.oidc.test;
22

3+
import jakarta.enterprise.event.Observes;
34
import jakarta.inject.Inject;
45
import jakarta.ws.rs.GET;
56
import jakarta.ws.rs.Path;
@@ -15,6 +16,7 @@
1516
import io.quarkus.test.common.QuarkusTestResource;
1617
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
1718
import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication;
19+
import io.quarkus.vertx.http.security.HttpSecurity;
1820
import io.restassured.RestAssured;
1921

2022
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
@@ -23,7 +25,7 @@ public class ImplicitBasicAuthAndBearerAuthCombinationTest {
2325
@RegisterExtension
2426
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
2527
.withApplicationRoot((jar) -> jar
26-
.addClasses(BasicBearerResource.class)
28+
.addClasses(BasicBearerResource.class, BearerPathBasedResource.class)
2729
.addAsResource(
2830
new StringAsset("""
2931
quarkus.security.users.embedded.enabled=true
@@ -41,10 +43,14 @@ public void testBasicEnabledAsSelectedWithAnnotation() {
4143
// endpoint is annotated with 'BasicAuthentication', so basic auth must be enabled
4244
RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/bearer")
4345
.then().statusCode(200).body(Matchers.is("alice"));
46+
RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/bearer-path-based")
47+
.then().statusCode(200).body(Matchers.is("alice"));
4448
RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/basic")
4549
.then().statusCode(204);
4650
RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/bearer")
4751
.then().statusCode(401);
52+
RestAssured.given().auth().basic("alice", "alice").get("/basic-bearer/bearer-path-based")
53+
.then().statusCode(401);
4854
RestAssured.given().auth().oauth2(getAccessToken()).get("/basic-bearer/basic")
4955
.then().statusCode(401);
5056
}
@@ -72,6 +78,23 @@ public String basic() {
7278
public String bearer() {
7379
return accessToken.getName();
7480
}
81+
82+
}
83+
84+
@Path("/basic-bearer/bearer-path-based")
85+
public static class BearerPathBasedResource {
86+
87+
@Inject
88+
JsonWebToken accessToken;
89+
90+
@GET
91+
public String bearerPathBased() {
92+
return accessToken.getName();
93+
}
94+
95+
void selectBearerUsingPathRule(@Observes HttpSecurity httpSecurity) {
96+
httpSecurity.path("/basic-bearer/bearer-path-based").bearer();
97+
}
7598
}
7699

77100
}

extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
import io.quarkus.deployment.Capability;
5151
import io.quarkus.deployment.annotations.BuildProducer;
5252
import io.quarkus.deployment.annotations.BuildStep;
53+
import io.quarkus.deployment.annotations.Consume;
5354
import io.quarkus.deployment.annotations.ExecutionTime;
5455
import io.quarkus.deployment.annotations.Produce;
5556
import io.quarkus.deployment.annotations.Record;
5657
import io.quarkus.deployment.builditem.ApplicationIndexBuildItem;
5758
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
59+
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
5860
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
5961
import io.quarkus.gizmo.ClassCreator;
6062
import io.quarkus.gizmo.DescriptorUtils;
@@ -265,12 +267,14 @@ void createHttpAuthenticationHandler(HttpSecurityRecorder recorder, Capabilities
265267
}
266268
}
267269

270+
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
268271
@Produce(PreRouterFinalizationBuildItem.class)
269272
@Record(ExecutionTime.RUNTIME_INIT)
270273
@BuildStep
271-
void initializeAuthenticationHandler(Optional<HttpAuthenticationHandlerBuildItem> authenticationHandler,
274+
void initializeHttpSecurity(Optional<HttpAuthenticationHandlerBuildItem> authenticationHandler,
272275
HttpSecurityRecorder recorder, VertxHttpConfig httpConfig, BeanContainerBuildItem beanContainerBuildItem) {
273276
if (authenticationHandler.isPresent()) {
277+
recorder.prepareHttpSecurityConfiguration(httpConfig);
274278
recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler, httpConfig,
275279
beanContainerBuildItem.getValue());
276280
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.quarkus.vertx.http.security;
2+
3+
import org.junit.jupiter.api.extension.RegisterExtension;
4+
5+
import io.quarkus.test.QuarkusUnitTest;
6+
7+
public class ConfigBasedPathMatchingHttpSecurityPolicyTest extends PathMatchingHttpSecurityPolicyTest {
8+
9+
private static final String APP_PROPS = """
10+
quarkus.http.auth.permission.authenticated.paths=/
11+
quarkus.http.auth.permission.authenticated.policy=authenticated
12+
quarkus.http.auth.permission.public.paths=/api*
13+
quarkus.http.auth.permission.public.policy=permit
14+
quarkus.http.auth.permission.foo.paths=/api/foo/bar
15+
quarkus.http.auth.permission.foo.policy=authenticated
16+
quarkus.http.auth.permission.unsecured.paths=/api/public
17+
quarkus.http.auth.permission.unsecured.policy=permit
18+
quarkus.http.auth.permission.inner-wildcard.paths=/api/*/bar
19+
quarkus.http.auth.permission.inner-wildcard.policy=authenticated
20+
quarkus.http.auth.permission.inner-wildcard2.paths=/api/next/*/prev
21+
quarkus.http.auth.permission.inner-wildcard2.policy=authenticated
22+
quarkus.http.auth.permission.inner-wildcard3.paths=/api/one/*/three/*
23+
quarkus.http.auth.permission.inner-wildcard3.policy=authenticated
24+
quarkus.http.auth.permission.inner-wildcard4.paths=/api/one/*/*/five
25+
quarkus.http.auth.permission.inner-wildcard4.policy=authenticated
26+
quarkus.http.auth.permission.inner-wildcard5.paths=/api/one/*/jamaica/*
27+
quarkus.http.auth.permission.inner-wildcard5.policy=permit
28+
quarkus.http.auth.permission.inner-wildcard6.paths=/api/*/sadly/*/dont-know
29+
quarkus.http.auth.permission.inner-wildcard6.policy=deny
30+
quarkus.http.auth.permission.baz.paths=/api/baz
31+
quarkus.http.auth.permission.baz.policy=authenticated
32+
quarkus.http.auth.permission.static-resource.paths=/static-file.html
33+
quarkus.http.auth.permission.static-resource.policy=authenticated
34+
quarkus.http.auth.permission.fubar.paths=/api/fubar/baz*
35+
quarkus.http.auth.permission.fubar.policy=authenticated
36+
quarkus.http.auth.permission.management.paths=/q/*
37+
quarkus.http.auth.permission.management.policy=authenticated
38+
quarkus.http.auth.policy.shared1.roles.root=admin,user
39+
quarkus.http.auth.permission.shared1.paths=/secured/*
40+
quarkus.http.auth.permission.shared1.policy=shared1
41+
quarkus.http.auth.permission.shared1.shared=true
42+
quarkus.http.auth.policy.unshared1.roles-allowed=user
43+
quarkus.http.auth.permission.unshared1.paths=/secured/user/*
44+
quarkus.http.auth.permission.unshared1.policy=unshared1
45+
quarkus.http.auth.policy.unshared2.roles-allowed=admin
46+
quarkus.http.auth.permission.unshared2.paths=/secured/admin/*
47+
quarkus.http.auth.permission.unshared2.policy=unshared2
48+
quarkus.http.auth.permission.shared2.paths=/*
49+
quarkus.http.auth.permission.shared2.shared=true
50+
quarkus.http.auth.permission.shared2.policy=custom
51+
quarkus.http.auth.roles-mapping.root1=admin,user
52+
quarkus.http.auth.roles-mapping.admin1=admin
53+
quarkus.http.auth.roles-mapping.public1=public2
54+
""";
55+
56+
@RegisterExtension
57+
static QuarkusUnitTest test = createQuarkusUnitTest(APP_PROPS);
58+
59+
}

0 commit comments

Comments
 (0)