diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java index 007fbb74b414e..87b13fd62d288 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import jakarta.enterprise.event.Observes; @@ -22,6 +23,7 @@ import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; @@ -38,7 +40,7 @@ public abstract class AbstractSecurityEventTest { protected static final Class[] TEST_CLASSES = { RolesAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class, UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class, UnsecuredResourceInterface.class, - UnsecuredParentResource.class + UnsecuredParentResource.class, RolesAllowedService.class, RolesAllowedServiceResource.class }; @Inject @@ -95,6 +97,99 @@ public void testRolesAllowed() { assertAsyncAuthZFailureObserved(2); } + @Test + public void testNestedRolesAllowed() { + // there are 2 different checks in place: user & admin on resource, admin on service + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/hello").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_HELLO)); + assertSyncObserved(3); + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties().get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/hello")); + // authorization success on endpoint + AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + identity = authZSuccessEvent.getSecurityIdentity(); + assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedServiceResource#getServiceHello", + securedMethod); + // authorization success on service level performed by CDI interceptor + authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(2); + securedMethod = (String) authZSuccessEvent.getEventProperties().get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#hello", securedMethod); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + assertAsyncAuthZFailureObserved(0); + RestAssured.given().auth().preemptive().basic("user", "user").get("/roles-service/hello").then().statusCode(403); + assertSyncObserved(6); + // "roles-service" Jakarta REST resource requires 'admin' or 'user' role, therefore check succeeds + successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(3); + identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("user", identity.getPrincipal().getName()); + routingContext = (RoutingContext) successEvent.getEventProperties().get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/hello")); + authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(4); + assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); + assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + // RolesService requires 'admin' role, therefore user fails + assertAsyncAuthZFailureObserved(1); + AuthorizationFailureEvent authZFailureEvent = observer.asyncAuthZFailureEvents.get(0); + securedMethod = (String) authZFailureEvent.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#hello", securedMethod); + SecurityIdentity userIdentity = authZFailureEvent.getSecurityIdentity(); + assertNotNull(userIdentity); + assertTrue(userIdentity.hasRole("user")); + assertEquals("user", userIdentity.getPrincipal().getName()); + assertNotNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); + assertEquals(RolesAllowedCheck.class.getName(), authZFailureEvent.getAuthorizationContext()); + } + + @Test + public void testNestedPermitAll() { + // @PermitAll is on CDI bean but resource is not secured + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/bye").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_BYE)); + final int expectedEventsCount; + if (isProactiveAuth()) { + // auth + @PermitAll + expectedEventsCount = 2; + } else { + // @PermitAll + expectedEventsCount = 1; + } + assertSyncObserved(expectedEventsCount, true); + + if (expectedEventsCount == 2) { + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties() + .get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/bye")); + } + // authorization success on service level performed by CDI interceptor + var authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(expectedEventsCount - 1); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.RolesAllowedService#bye", securedMethod); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + if (isProactiveAuth()) { + assertNotNull(authZSuccessEvent.getSecurityIdentity()); + assertEquals("admin", authZSuccessEvent.getSecurityIdentity().getPrincipal().getName()); + } + assertAsyncAuthZFailureObserved(0); + } + @Test public void testAuthenticated() { RestAssured.given().auth().preemptive().basic("admin", "admin").get("/unsecured/authenticated").then().statusCode(200) @@ -115,6 +210,8 @@ public void testAuthenticated() { SecurityIdentity anonymousIdentity = authZFailure.getSecurityIdentity(); assertNotNull(anonymousIdentity); assertTrue(anonymousIdentity.isAnonymous()); + String securedMethod = (String) authZFailure.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.test.security.UnsecuredResource#authenticated", securedMethod); } @Test @@ -152,8 +249,7 @@ public void testPermitAll() { assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/permitAll")); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); - // SecurityIdentity is not required for the permit all check - assertNull(authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getSecurityIdentity()); } else { assertSyncObserved(1, true); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(0); @@ -186,6 +282,9 @@ private void assertAsyncAuthZFailureObserved(int count) { .untilAsserted(() -> assertEquals(count, observer.asyncAuthZFailureEvents.size())); if (count > 0) { assertTrue(observer.asyncAuthZFailureEvents.stream().allMatch(e -> e.getSecurityIdentity() != null)); + assertTrue(observer.asyncAuthZFailureEvents.stream() + .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) + .allMatch(Objects::nonNull)); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java new file mode 100644 index 0000000000000..36931b20d44cd --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedService.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RolesAllowedService { + + public static final String SERVICE_HELLO = "Hello from Service!"; + public static final String SERVICE_BYE = "Bye from Service!"; + + @RolesAllowed("admin") + public String hello() { + return SERVICE_HELLO; + } + + @PermitAll + public String bye() { + return SERVICE_BYE; + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java new file mode 100644 index 0000000000000..f621b73b0da64 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/RolesAllowedServiceResource.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/roles-service") +public class RolesAllowedServiceResource { + + @Inject + RolesAllowedService rolesAllowedService; + + @Path("/hello") + @RolesAllowed({ "user", "admin" }) + @GET + public String getServiceHello() { + return rolesAllowedService.hello(); + } + + @Path("/bye") + @GET + public String getServiceBye() { + return rolesAllowedService.bye(); + } + +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java index 950a3c355fe91..5453a44eedd65 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java @@ -22,7 +22,9 @@ import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.vertx.ext.web.RoutingContext; @Priority(Priorities.AUTHENTICATION) @@ -35,7 +37,7 @@ public class EagerSecurityFilter implements ContainerRequestFilter { ResourceInfo resourceInfo; @Inject - RoutingContext routingContext; + CurrentVertxRequest currentVertxRequest; @Inject SecurityCheckStorage securityCheckStorage; @@ -71,19 +73,27 @@ public void filter(ContainerRequestContext requestContext) throws IOException { private void applySecurityChecks(MethodDescription description) { SecurityCheck check = securityCheckStorage.getSecurityCheck(description); if (check == null && securityCheckStorage.getDefaultSecurityCheck() != null - && routingContext.get(EagerSecurityFilter.class.getName()) == null - && routingContext.get(SKIP_DEFAULT_CHECK) == null) { + && routingContext().get(EagerSecurityFilter.class.getName()) == null + && routingContext().get(SKIP_DEFAULT_CHECK) == null) { check = securityCheckStorage.getDefaultSecurityCheck(); } if (check != null) { if (check.isPermitAll()) { - fireEventOnAuthZSuccess(check, null); + // add the identity only if authentication has already finished + final SecurityIdentity identity; + if (routingContext().user() instanceof QuarkusHttpUser user) { + identity = user.getSecurityIdentity(); + } else { + identity = null; + } + + fireEventOnAuthZSuccess(check, identity, description); } else { if (check.requiresMethodArguments()) { if (identityAssociation.getIdentity().isAnonymous()) { var exception = new UnauthorizedException(); if (jaxRsPermissionChecker.getEventHelper().fireEventOnFailure()) { - fireEventOnAuthZFailure(exception, check); + fireEventOnAuthZFailure(exception, check, description); } throw exception; } @@ -94,36 +104,43 @@ private void applySecurityChecks(MethodDescription description) { try { check.apply(identityAssociation.getIdentity(), description, null); } catch (Exception e) { - fireEventOnAuthZFailure(e, check); + fireEventOnAuthZFailure(e, check, description); throw e; } } else { check.apply(identityAssociation.getIdentity(), description, null); } - fireEventOnAuthZSuccess(check, identityAssociation.getIdentity()); + fireEventOnAuthZSuccess(check, identityAssociation.getIdentity(), description); } // prevent repeated security checks - routingContext.put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod()); + routingContext().put(EagerSecurityFilter.class.getName(), resourceInfo.getResourceMethod()); } } - private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check) { + private void fireEventOnAuthZFailure(Exception exception, SecurityCheck check, MethodDescription description) { jaxRsPermissionChecker.getEventHelper().fireFailureEvent(new AuthorizationFailureEvent( identityAssociation.getIdentity(), exception, check.getClass().getName(), - Map.of(RoutingContext.class.getName(), routingContext))); + Map.of(RoutingContext.class.getName(), routingContext()), description)); } - private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity) { + private void fireEventOnAuthZSuccess(SecurityCheck check, SecurityIdentity securityIdentity, + MethodDescription description) { if (jaxRsPermissionChecker.getEventHelper().fireEventOnSuccess()) { jaxRsPermissionChecker.getEventHelper().fireSuccessEvent(new AuthorizationSuccessEvent(securityIdentity, - check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext))); + check.getClass().getName(), Map.of(RoutingContext.class.getName(), routingContext()), description)); } } + private RoutingContext routingContext() { + // use actual RoutingContext (not the bean) to async events are invoked with new CDI request context + // where the RoutingContext is not available + return currentVertxRequest.getCurrent(); + } + private void applyEagerSecurityInterceptors(MethodDescription description) { var interceptor = interceptorStorage.getInterceptor(description); if (interceptor != null) { - interceptor.accept(routingContext); + interceptor.accept(routingContext()); } } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java index 1d3366fd2220a..b88c862e5f85b 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractSecurityEventTest.java @@ -130,10 +130,16 @@ public void testNestedRolesAllowed() { assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); identity = authZSuccessEvent.getSecurityIdentity(); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedServiceResource#getServiceHello", + securedMethod); // authorization success on service level performed by CDI interceptor authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(2); + securedMethod = (String) authZSuccessEvent.getEventProperties().get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#hello", securedMethod); assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); - assertNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); assertAsyncAuthZFailureObserved(0); RestAssured.given().auth().preemptive().basic("user", "user").get("/roles-service/hello").then().statusCode(403); assertSyncObserved(6, false, false); @@ -149,18 +155,56 @@ public void testNestedRolesAllowed() { assertEquals(identity, authZSuccessEvent.getSecurityIdentity()); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); // RolesService requires 'admin' role, therefore user fails - // here security check is performed on CDI bean by security interceptor, therefore no RoutingContext is added - assertAsyncAuthZFailureObserved(1, false); + assertAsyncAuthZFailureObserved(1); AuthorizationFailureEvent authZFailureEvent = observer.asyncAuthZFailureEvents.get(0); + securedMethod = (String) authZFailureEvent.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#hello", securedMethod); SecurityIdentity userIdentity = authZFailureEvent.getSecurityIdentity(); assertNotNull(userIdentity); assertTrue(userIdentity.hasRole("user")); assertEquals("user", userIdentity.getPrincipal().getName()); - // there is no RoutingContext as the check is performed by security interceptor - assertNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); + assertNotNull(authZFailureEvent.getEventProperties().get(RoutingContext.class.getName())); assertEquals(RolesAllowedCheck.class.getName(), authZFailureEvent.getAuthorizationContext()); } + @Test + public void testNestedPermitAll() { + // @PermitAll is on CDI bean but resource is not secured + RestAssured.given().auth().preemptive().basic("admin", "admin").get("/roles-service/bye").then().statusCode(200) + .body(is(RolesAllowedService.SERVICE_BYE)); + final int expectedEventsCount; + if (isProactiveAuth()) { + // auth + @PermitAll + expectedEventsCount = 2; + } else { + // @PermitAll + expectedEventsCount = 1; + } + assertSyncObserved(expectedEventsCount, true, true); + + if (expectedEventsCount == 2) { + AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) observer.syncEvents.get(0); + SecurityIdentity identity = successEvent.getSecurityIdentity(); + assertNotNull(identity); + assertEquals("admin", identity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) successEvent.getEventProperties() + .get(RoutingContext.class.getName()); + assertNotNull(routingContext); + assertTrue(routingContext.request().path().endsWith("/roles-service/bye")); + } + // authorization success on service level performed by CDI interceptor + var authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(expectedEventsCount - 1); + String securedMethod = (String) authZSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.RolesAllowedService#bye", securedMethod); + assertNotNull(authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); + if (isProactiveAuth()) { + assertNotNull(authZSuccessEvent.getSecurityIdentity()); + assertEquals("admin", authZSuccessEvent.getSecurityIdentity().getPrincipal().getName()); + } + assertAsyncAuthZFailureObserved(0); + } + @Test public void testAuthenticated() { RestAssured.given().auth().preemptive().basic("admin", "admin").get("/unsecured/authenticated").then().statusCode(200) @@ -186,6 +230,8 @@ public void testAuthenticated() { routingContext = (RoutingContext) authZFailure.getEventProperties().get(RoutingContext.class.getName()); assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/authenticated")); + String securedMethod = (String) authZFailure.getEventProperties().get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + assertEquals("io.quarkus.resteasy.reactive.server.test.security.UnsecuredResource#authenticated", securedMethod); } @Test @@ -227,8 +273,7 @@ public void testPermitAll() { assertNotNull(routingContext); assertTrue(routingContext.request().path().endsWith("/unsecured/permitAll")); AuthorizationSuccessEvent authZSuccessEvent = (AuthorizationSuccessEvent) observer.syncEvents.get(1); - // SecurityIdentity is not required for the permit all check - assertNull(authZSuccessEvent.getSecurityIdentity()); + assertNotNull(authZSuccessEvent.getSecurityIdentity()); assertEquals(routingContext, authZSuccessEvent.getEventProperties().get(RoutingContext.class.getName())); } else { assertSyncObserved(1, true, true); @@ -263,19 +308,13 @@ private void assertSyncObserved(int count, boolean expectRoutingContext, boolean } private void assertAsyncAuthZFailureObserved(int count) { - assertAsyncAuthZFailureObserved(count, true); - } - - private void assertAsyncAuthZFailureObserved(int count, boolean expectRoutingContext) { Awaitility.await().atMost(Duration.ofSeconds(2)) .untilAsserted(() -> assertEquals(count, observer.asyncAuthZFailureEvents.size())); if (count > 0) { assertTrue(observer.asyncAuthZFailureEvents.stream().allMatch(e -> e.getSecurityIdentity() != null)); - if (expectRoutingContext) { - assertTrue(observer.asyncAuthZFailureEvents.stream() - .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) - .allMatch(Objects::nonNull)); - } + assertTrue(observer.asyncAuthZFailureEvents.stream() + .map(e -> e.getEventProperties().get(RoutingContext.class.getName())) + .allMatch(Objects::nonNull)); } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java index 23c1203c114e8..7a4d5e2a247bd 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedService.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.server.test.security; +import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.ApplicationScoped; @@ -7,10 +8,16 @@ public class RolesAllowedService { public static final String SERVICE_HELLO = "Hello from Service!"; + public static final String SERVICE_BYE = "Bye from Service!"; @RolesAllowed("admin") public String hello() { return SERVICE_HELLO; } + @PermitAll + public String bye() { + return SERVICE_BYE; + } + } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java index 6ded9428be1b0..660dfad5adcad 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedServiceResource.java @@ -18,4 +18,9 @@ public String getServiceHello() { return rolesAllowedService.hello(); } + @Path("/bye") + @GET + public String getServiceBye() { + return rolesAllowedService.bye(); + } } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java index 59b74d611bcb5..27da712905fe3 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/EagerSecurityHandler.java @@ -23,6 +23,7 @@ import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniSubscriber; import io.smallrye.mutiny.subscription.UniSubscription; @@ -132,8 +133,18 @@ private Function> getSecurityCheck(ResteasyReactiveRequ preventRepeatedSecurityChecks(requestContext, methodDescription); if (EagerSecurityContext.instance.eventHelper.fireEventOnSuccess()) { requestContext.requireCDIRequestScope(); - EagerSecurityContext.instance.eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(null, - check.getClass().getName(), createEventPropsWithRoutingCtx(requestContext))); + + // add the identity only if authentication has already finished + final SecurityIdentity identity; + var event = requestContext.unwrap(RoutingContext.class); + if (event != null && event.user() instanceof QuarkusHttpUser user) { + identity = user.getSecurityIdentity(); + } else { + identity = null; + } + + EagerSecurityContext.instance.eventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(identity, + check.getClass().getName(), createEventPropsWithRoutingCtx(requestContext), methodDescription)); } return null; } else { @@ -156,7 +167,8 @@ public Uni apply(SecurityIdentity securityIdentity) { if (EagerSecurityContext.instance.eventHelper.fireEventOnFailure()) { EagerSecurityContext.instance.eventHelper .fireFailureEvent(new AuthorizationFailureEvent(securityIdentity, unauthorizedException, - theCheck.getClass().getName(), createEventPropsWithRoutingCtx(requestContext))); + theCheck.getClass().getName(), createEventPropsWithRoutingCtx(requestContext), + methodDescription)); } throw unauthorizedException; } @@ -175,7 +187,7 @@ public void accept(Throwable throwable) { EagerSecurityContext.instance.eventHelper .fireFailureEvent(new AuthorizationFailureEvent( securityIdentity, throwable, theCheck.getClass().getName(), - createEventPropsWithRoutingCtx(requestContext))); + createEventPropsWithRoutingCtx(requestContext), methodDescription)); } }); } @@ -187,7 +199,7 @@ public void run() { EagerSecurityContext.instance.eventHelper.fireSuccessEvent( new AuthorizationSuccessEvent(securityIdentity, theCheck.getClass().getName(), - createEventPropsWithRoutingCtx(requestContext))); + createEventPropsWithRoutingCtx(requestContext), methodDescription)); } }); } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index ce61a24f2eeae..6fdbd00c2117c 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -34,6 +34,7 @@ import java.util.function.Predicate; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -68,6 +69,7 @@ import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -105,6 +107,7 @@ import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; +import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem; import io.quarkus.security.spi.DefaultSecurityCheckBuildItem; import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem; import io.quarkus.security.spi.runtime.AuthorizationController; @@ -458,14 +461,38 @@ private static List registerProvider(String providerName, return providerClasses; } + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void recordRuntimeConfigReady(SecurityCheckRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem, + LaunchModeBuildItem launchModeBuildItem) { + recorder.setRuntimeConfigReady(); + if (launchModeBuildItem.getLaunchMode() == LaunchMode.DEVELOPMENT) { + recorder.unsetRuntimeConfigReady(shutdownContextBuildItem); + } + } + + @Record(ExecutionTime.STATIC_INIT) @BuildStep void registerSecurityInterceptors(BuildProducer registrars, - BuildProducer beans) { + BuildProducer beans, + BuildProducer syntheticBeanProducer, SecurityCheckRecorder recorder, + Optional additionalSecurityConstrainerEventsItem) { registrars.produce(new InterceptorBindingRegistrarBuildItem(new SecurityAnnotationsRegistrar())); Class[] interceptors = { AuthenticatedInterceptor.class, DenyAllInterceptor.class, PermitAllInterceptor.class, RolesAllowedInterceptor.class, PermissionsAllowedInterceptor.class }; beans.produce(new AdditionalBeanBuildItem(interceptors)); - beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class, SecurityConstrainer.class)); + beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class)); + + var additionalEventsSupplier = additionalSecurityConstrainerEventsItem + .map(AdditionalSecurityConstrainerEventPropsBuildItem::getAdditionalEventPropsSupplier) + .orElse(null); + syntheticBeanProducer.produce(SyntheticBeanBuildItem + .configure(SecurityConstrainer.class) + .unremovable() + .scope(Singleton.class) + .supplier(recorder.createSecurityConstrainer(additionalEventsSupplier)) + .done()); } /** diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java index b4bfa041c50bb..fcfa902bc6093 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AbstractSecurityEvent.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import io.quarkus.security.identity.SecurityIdentity; @@ -25,15 +26,28 @@ public Map getEventProperties() { return eventProperties; } + protected static String toString(MethodDescription methodDescription) { + Objects.requireNonNull(methodDescription); + return methodDescription.getClassName() + "#" + methodDescription.getMethodName(); + } + protected static Map withProperties(String propertyKey, Object propertyValue, Map additionalProperties) { - final Map result = new HashMap<>(); + + final HashMap result; + if (additionalProperties instanceof HashMap additionalPropertiesHashMap) { + // do not recreate map when multiple props are added + result = additionalPropertiesHashMap; + } else { + result = new HashMap<>(); + if (additionalProperties != null && !additionalProperties.isEmpty()) { + result.putAll(additionalProperties); + } + } + if (propertyValue != null) { result.put(propertyKey, propertyValue); } - if (additionalProperties != null && !additionalProperties.isEmpty()) { - result.putAll(additionalProperties); - } return result; } } diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java index 30f9286cba7ce..caf5cea6e716e 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationFailureEvent.java @@ -12,6 +12,7 @@ public final class AuthorizationFailureEvent extends AbstractSecurityEvent { public static final String AUTHORIZATION_FAILURE_KEY = AuthorizationFailureEvent.class.getName() + ".FAILURE"; public static final String AUTHORIZATION_CONTEXT_KEY = AuthorizationFailureEvent.class.getName() + ".CONTEXT"; + public static final String SECURED_METHOD_KEY = AuthorizationFailureEvent.class.getName() + ".SECURED_METHOD"; public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable authorizationFailure, String authorizationContext) { @@ -23,6 +24,12 @@ public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable au super(securityIdentity, withProperties(authorizationFailure, authorizationContext, eventProperties)); } + public AuthorizationFailureEvent(SecurityIdentity securityIdentity, Throwable authorizationFailure, + String authorizationContext, Map eventProperties, MethodDescription securedMethod) { + this(securityIdentity, authorizationFailure, authorizationContext, + withProperties(SECURED_METHOD_KEY, toString(securedMethod), eventProperties)); + } + public Throwable getAuthorizationFailure() { return (Throwable) eventProperties.get(AUTHORIZATION_FAILURE_KEY); } diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java index 2160d74b87724..69fd6cbb9b002 100644 --- a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationSuccessEvent.java @@ -10,6 +10,7 @@ */ public final class AuthorizationSuccessEvent extends AbstractSecurityEvent { public static final String AUTHORIZATION_CONTEXT = AuthorizationSuccessEvent.class.getName() + ".CONTEXT"; + public static final String SECURED_METHOD_KEY = AuthorizationSuccessEvent.class.getName() + ".SECURED_METHOD"; public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, Map eventProperties) { super(securityIdentity, eventProperties); @@ -19,4 +20,10 @@ public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, String autho Map eventProperties) { super(securityIdentity, withProperties(AUTHORIZATION_CONTEXT, authorizationContext, eventProperties)); } + + public AuthorizationSuccessEvent(SecurityIdentity securityIdentity, String authorizationContext, + Map eventProperties, MethodDescription securedMethod) { + this(securityIdentity, authorizationContext, withProperties(SECURED_METHOD_KEY, toString(securedMethod), + eventProperties)); + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 545b3249cd9b0..732bda8773e80 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -17,16 +18,21 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import io.quarkus.arc.Arc; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.StringPermission; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; +import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; import io.quarkus.security.runtime.interceptor.check.DenyAllCheck; import io.quarkus.security.runtime.interceptor.check.PermissionSecurityCheck; import io.quarkus.security.runtime.interceptor.check.PermitAllCheck; import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; import io.quarkus.security.runtime.interceptor.check.SupplierRolesAllowedCheck; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.smallrye.config.Expressions; @@ -37,6 +43,7 @@ public class SecurityCheckRecorder { private static volatile SecurityCheckStorage storage; private static final Set configExpRolesAllowedChecks = ConcurrentHashMap.newKeySet(); + private static volatile boolean runtimeConfigReady = false; public static SecurityCheckStorage getStorage() { return storage; @@ -355,4 +362,37 @@ private Class loadClass(String className) { public void registerDefaultSecurityCheck(RuntimeValue builder, SecurityCheck securityCheck) { builder.getValue().registerDefaultSecurityCheck(securityCheck); } + + public Supplier createSecurityConstrainer(Supplier> additionalEventPropsSupplier) { + return new Supplier() { + @Override + public SecurityConstrainer get() { + var container = Arc.container(); + var beanManager = container.beanManager(); + var eventPropsSupplier = additionalEventPropsSupplier == null ? new Supplier>() { + @Override + public Map get() { + return Map.of(); + } + } : additionalEventPropsSupplier; + return new SecurityConstrainer(container.instance(SecurityCheckStorage.class).get(), + beanManager, beanManager.getEvent().select(AuthorizationFailureEvent.class), + beanManager.getEvent().select(AuthorizationSuccessEvent.class), runtimeConfigReady, + container.select(SecurityIdentityAssociation.class), eventPropsSupplier); + } + }; + } + + public void setRuntimeConfigReady() { + runtimeConfigReady = true; + } + + public void unsetRuntimeConfigReady(ShutdownContext shutdownContext) { + shutdownContext.addShutdownTask(new Runnable() { + @Override + public void run() { + runtimeConfigReady = false; + } + }); + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java index a8d68b3c23e48..3d63ca29d80e4 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java @@ -4,19 +4,24 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS; import java.lang.reflect.Method; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import jakarta.enterprise.event.Event; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import org.eclipse.microprofile.config.ConfigProvider; + import io.quarkus.runtime.BlockingOperationNotAllowedException; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.SecurityIdentityAssociation; import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.quarkus.security.spi.runtime.SecurityEventHelper; @@ -31,16 +36,26 @@ public class SecurityConstrainer { public static final Object CHECK_OK = new Object(); private final SecurityCheckStorage storage; private final SecurityEventHelper securityEventHelper; + private final Instance securityIdentityAssociation; + private final Supplier> additionalEventPropsSupplier; - @Inject - SecurityIdentityAssociation identityAssociation; - - SecurityConstrainer(SecurityCheckStorage storage, BeanManager beanManager, - Event authZFailureEvent, Event authZSuccessEvent) { + public SecurityConstrainer(SecurityCheckStorage storage, BeanManager beanManager, + Event authZFailureEvent, Event authZSuccessEvent, + boolean runtimeConfigReady, Instance securityIdentityAssociation, + Supplier> additionalEventPropsSupplier) { + this.securityIdentityAssociation = securityIdentityAssociation; + this.additionalEventPropsSupplier = additionalEventPropsSupplier; this.storage = storage; - // static interceptors are initialized during the static init, therefore we need to initialize the helper lazily - this.securityEventHelper = SecurityEventHelper.lazilyOf(authZSuccessEvent, authZFailureEvent, - AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE, beanManager); + if (runtimeConfigReady) { + boolean securityEventsEnabled = ConfigProvider.getConfig().getValue("quarkus.security.events.enabled", + Boolean.class); + this.securityEventHelper = new SecurityEventHelper<>(authZSuccessEvent, authZFailureEvent, AUTHORIZATION_SUCCESS, + AUTHORIZATION_FAILURE, beanManager, securityEventsEnabled); + } else { + // static interceptors are initialized during the static init, therefore we need to initialize the helper lazily + this.securityEventHelper = SecurityEventHelper.lazilyOf(authZSuccessEvent, authZFailureEvent, + AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE, beanManager); + } } public void check(Method method, Object[] parameters) { @@ -48,7 +63,7 @@ public void check(Method method, Object[] parameters) { SecurityIdentity identity = null; if (securityCheck != null && !securityCheck.isPermitAll()) { try { - identity = identityAssociation.getIdentity(); + identity = securityIdentityAssociation.get().getIdentity(); } catch (BlockingOperationNotAllowedException blockingException) { throw new BlockingOperationNotAllowedException( "Blocking security check attempted in code running on the event loop. " + @@ -61,7 +76,7 @@ public void check(Method method, Object[] parameters) { try { securityCheck.apply(identity, method, parameters); } catch (Exception exception) { - fireAuthZFailureEvent(identity, exception, securityCheck); + fireAuthZFailureEvent(identity, exception, securityCheck, method); throw exception; } } else { @@ -69,7 +84,7 @@ public void check(Method method, Object[] parameters) { } } if (securityEventHelper.fireEventOnSuccess()) { - fireAuthZSuccessEvent(securityCheck, identity); + fireAuthZSuccessEvent(securityCheck, identity, method); } } @@ -77,7 +92,7 @@ public Uni nonBlockingCheck(Method method, Object[] parameters) { SecurityCheck securityCheck = storage.getSecurityCheck(method); if (securityCheck != null) { if (!securityCheck.isPermitAll()) { - return identityAssociation.getDeferredIdentity() + return securityIdentityAssociation.get().getDeferredIdentity() .onItem() .transformToUni(new Function>() { @Override @@ -87,7 +102,7 @@ public Uni apply(SecurityIdentity securityIdentity) { checkResult = checkResult.onFailure().invoke(new Consumer() { @Override public void accept(Throwable throwable) { - fireAuthZFailureEvent(securityIdentity, throwable, securityCheck); + fireAuthZFailureEvent(securityIdentity, throwable, securityCheck, method); } }); } @@ -95,7 +110,7 @@ public void accept(Throwable throwable) { checkResult = checkResult.invoke(new Runnable() { @Override public void run() { - fireAuthZSuccessEvent(securityCheck, securityIdentity); + fireAuthZSuccessEvent(securityCheck, securityIdentity, method); } }); } @@ -103,19 +118,28 @@ public void run() { } }); } else if (securityEventHelper.fireEventOnSuccess()) { - fireAuthZSuccessEvent(securityCheck, null); + fireAuthZSuccessEvent(securityCheck, null, method); } } return Uni.createFrom().item(CHECK_OK); } - private void fireAuthZSuccessEvent(SecurityCheck securityCheck, SecurityIdentity identity) { + private void fireAuthZSuccessEvent(SecurityCheck securityCheck, SecurityIdentity identity, Method method) { var securityCheckName = securityCheck == null ? null : securityCheck.getClass().getName(); - securityEventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(identity, securityCheckName, null)); + var additionalEventProps = additionalEventPropsSupplier.get(); + if (identity == null) { + // get identity from event if auth already finished + identity = (SecurityIdentity) additionalEventProps.get(SecurityIdentity.class.getName()); + } + securityEventHelper.fireSuccessEvent( + new AuthorizationSuccessEvent(identity, securityCheckName, additionalEventPropsSupplier.get(), + MethodDescription.ofMethod(method))); } - private void fireAuthZFailureEvent(SecurityIdentity identity, Throwable failure, SecurityCheck securityCheck) { + private void fireAuthZFailureEvent(SecurityIdentity identity, Throwable failure, SecurityCheck securityCheck, + Method method) { securityEventHelper - .fireFailureEvent(new AuthorizationFailureEvent(identity, failure, securityCheck.getClass().getName())); + .fireFailureEvent(new AuthorizationFailureEvent(identity, failure, securityCheck.getClass().getName(), + additionalEventPropsSupplier.get(), MethodDescription.ofMethod(method))); } } diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java new file mode 100644 index 0000000000000..29617d9a0c3af --- /dev/null +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecurityConstrainerEventPropsBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.security.spi; + +import java.util.Map; +import java.util.function.Supplier; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * This item allows to enhance properties of security events produced by SecurityConstrainer. + * The SecurityConstrainer is usually invoked when CDI request context is already fully setup, and the additional + * properties can be added based on the active context. + */ +public final class AdditionalSecurityConstrainerEventPropsBuildItem extends SimpleBuildItem { + + private final Supplier> additionalEventPropsSupplier; + + public AdditionalSecurityConstrainerEventPropsBuildItem(Supplier> additionalEventPropsSupplier) { + this.additionalEventPropsSupplier = additionalEventPropsSupplier; + } + + public Supplier> getAdditionalEventPropsSupplier() { + return additionalEventPropsSupplier; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 235aa3500c784..6c77581e02424 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -53,6 +53,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem; +import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem; import io.quarkus.security.spi.SecurityTransformerUtils; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -391,6 +392,16 @@ void produceEagerSecurityInterceptorStorage(HttpSecurityRecorder recorder, } } + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + void addRoutingCtxToSecurityEventsForCdiBeans(HttpSecurityRecorder recorder, Capabilities capabilities, + BuildProducer producer) { + if (capabilities.isPresent(Capability.SECURITY)) { + producer.produce( + new AdditionalSecurityConstrainerEventPropsBuildItem(recorder.createAdditionalSecEventPropsSupplier())); + } + } + private static void validateAuthMechanismAnnotationUsage(Capabilities capabilities, HttpBuildTimeConfig buildTimeConfig, DotName[] annotationNames) { if (buildTimeConfig.auth.proactive diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index dd1071ea40e27..512542b7f670b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -37,6 +37,7 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.spi.runtime.MethodDescription; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.CompositeException; import io.smallrye.mutiny.Uni; @@ -143,6 +144,30 @@ public String name() { }); } + public Supplier> createAdditionalSecEventPropsSupplier() { + return new Supplier>() { + @Override + public Map get() { + if (Arc.container().requestContext().isActive()) { + + // if present, add RoutingContext from CDI request to the SecurityEvents produced in Security extension + // it's done this way as Security extension is not Vert.x based, but users find RoutingContext useful + var event = Arc.container().instance(CurrentVertxRequest.class).get().getCurrent(); + if (event != null) { + + if (event.user() instanceof QuarkusHttpUser user) { + return Map.of(RoutingContext.class.getName(), event, SecurityIdentity.class.getName(), + user.getSecurityIdentity()); + } + + return Map.of(RoutingContext.class.getName(), event); + } + } + return Map.of(); + } + }; + } + public static abstract class DefaultAuthFailureHandler implements BiConsumer { protected DefaultAuthFailureHandler() {