From 7521bf2e25ae4c5f0f51dd8875af100611b261ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 22 Jan 2025 19:54:43 +0100 Subject: [PATCH] feat(ws next): fire authz events for HTTP upgrade sec. checks --- .../next/deployment/WebSocketProcessor.java | 8 +- ...HttpUpgradeRolesAllowedAnnotationTest.java | 21 +- .../WebSocketsSecurityEventsTest.java | 444 ++++++++++++++++++ .../runtime/SecurityHttpUpgradeCheck.java | 40 +- .../next/runtime/WebSocketServerRecorder.java | 25 +- 5 files changed, 526 insertions(+), 12 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/WebSocketsSecurityEventsTest.java diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java index c0ae258242cf2..6cdc288e30d55 100644 --- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.websockets.next.deployment; +import static io.quarkus.arc.processor.DotNames.EVENT; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import java.util.ArrayList; @@ -95,6 +96,8 @@ import io.quarkus.security.spi.ClassSecurityCheckStorageBuildItem; import io.quarkus.security.spi.PermissionsAllowedMetaAnnotationBuildItem; import io.quarkus.security.spi.SecurityTransformerUtils; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; @@ -680,7 +683,10 @@ void createSecurityHttpUpgradeCheck(BuildProducer produc .scope(BuiltinScope.SINGLETON.getInfo()) .priority(SecurityHttpUpgradeCheck.BEAN_PRIORITY) .setRuntimeInit() - .supplier(recorder.createSecurityHttpUpgradeCheck(endpointIdToSecurityCheck)) + .addInjectionPoint(ClassType.create(DotNames.BEAN_MANAGER)) + .addInjectionPoint(ParameterizedType.create(EVENT, ClassType.create(AuthorizationFailureEvent.class))) + .addInjectionPoint(ParameterizedType.create(EVENT, ClassType.create(AuthorizationSuccessEvent.class))) + .createWith(recorder.createSecurityHttpUpgradeCheck(endpointIdToSecurityCheck)) .done()); } } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/HttpUpgradeRolesAllowedAnnotationTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/HttpUpgradeRolesAllowedAnnotationTest.java index ff687fcab57ac..5e530e2206b25 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/HttpUpgradeRolesAllowedAnnotationTest.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/HttpUpgradeRolesAllowedAnnotationTest.java @@ -7,16 +7,20 @@ import java.net.URI; import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.runtime.util.ExceptionUtil; import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.CurrentIdentityAssociation; +import io.quarkus.security.spi.runtime.SecurityEvent; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; @@ -33,8 +37,11 @@ public class HttpUpgradeRolesAllowedAnnotationTest extends SecurityTestBase { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(""" + quarkus.security.events.enabled=false + """), "application.properties") .addClasses(Endpoint.class, WSClient.class, TestIdentityProvider.class, TestIdentityController.class, - AdminEndpoint.class)); + AdminEndpoint.class, SecurityEventObserver.class)); @TestHTTPResource("admin-end") URI adminEndpointUri; @@ -56,6 +63,9 @@ public void testInsufficientRights() { client.waitForMessages(2); assertEquals("hello", client.getMessages().get(1).toString()); } + + // assert no security events when the events are disabled + assertEquals(0, SecurityEventObserver.count.get()); } @RolesAllowed("admin") @@ -101,4 +111,13 @@ String error(ForbiddenException t) { } } + + public static class SecurityEventObserver { + + private static final AtomicInteger count = new AtomicInteger(); + + void observe(@Observes SecurityEvent securityEvent) { + count.incrementAndGet(); + } + } } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/WebSocketsSecurityEventsTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/WebSocketsSecurityEventsTest.java new file mode 100644 index 0000000000000..4df26a8c290ee --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/WebSocketsSecurityEventsTest.java @@ -0,0 +1,444 @@ +package io.quarkus.websockets.next.test.security; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.ObservesAsync; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.spi.runtime.AuthenticationFailureEvent; +import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent; +import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; +import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.runtime.SecurityHttpUpgradeCheck; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.UpgradeRejectedException; +import io.vertx.core.http.WebSocketConnectOptions; +import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; +import io.vertx.ext.web.RoutingContext; + +public class WebSocketsSecurityEventsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(""" + quarkus.http.auth.permission.roles.paths=/http-upgrade-config-endpoint* + quarkus.http.auth.permission.roles.policy=roles + quarkus.http.auth.policy.roles.roles-allowed=http-upgrade-config + """), "application.properties") + .addClasses(WSClient.class, TestIdentityProvider.class, TestIdentityController.class, + HttpUpgradeAnnotationEndpoint.class, HttpUpgradeConfigEndpoint.class, OnTextMessageEndpoint.class, + SecurityEventObserver.class)); + + @TestHTTPResource("http-upgrade-annotation-endpoint") + URI httpUpgradeAnnotationEndpoint; + + @TestHTTPResource("http-upgrade-config-endpoint") + URI httpUpgradeConfigEndpoint; + + @TestHTTPResource("on-text-message-endpoint") + URI onTextMessageAnnotationEndpoint; + + @Inject + Vertx vertx; + + @Inject + SecurityEventObserver eventObserver; + + @BeforeEach + public void clearEvents() { + eventObserver.clearEvents(); + } + + @Test + public void testHttpUpgradeSecuredWithAnnotationEvents() { + // user is missing role 'http-upgrade-annotation' -> fail + TestIdentityController.resetRoles().add("user", "user"); + try (WSClient client = new WSClient(vertx)) { + CompletionException ce = assertThrows(CompletionException.class, + () -> client.connect(basicAuth(), httpUpgradeAnnotationEndpoint)); + Throwable root = ExceptionUtil.getRootCause(ce); + assertInstanceOf(UpgradeRejectedException.class, root); + assertTrue(root.getMessage().contains("403")); + } + + assertEquals(1, eventObserver.authenticationSuccessEvents); + assertEquals(1, eventObserver.authenticationSuccessAsyncEvents); + assertEquals(1, eventObserver.authorizationFailureEvents); + assertEquals(1, eventObserver.authorizationFailureAsyncEvents); + assertEquals(0, eventObserver.authorizationSuccessEvents); + assertEquals(0, eventObserver.authorizationSuccessAsyncEvents); + assertEquals(0, eventObserver.authenticationFailureEvents); + assertEquals(0, eventObserver.authenticationFailureAsyncEvents); + assertAuthenticationSuccess(); + AuthorizationFailureEvent authorizationFailureEvent = assertAuthorizationFailureEvent( + eventObserver.authorizationFailureEvents); + HttpServerRequest httpServerRequest = (HttpServerRequest) authorizationFailureEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.HTTP_REQUEST_KEY); + Assertions.assertNotNull(httpServerRequest); + authorizationFailureEvent = assertAuthorizationFailureEvent(eventObserver.authorizationFailureAsyncEvents); + httpServerRequest = (HttpServerRequest) authorizationFailureEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.HTTP_REQUEST_KEY); + Assertions.assertNotNull(httpServerRequest); + String endpointId = (String) authorizationFailureEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.SECURED_ENDPOINT_ID_KEY); + Assertions.assertEquals("one-two-three", endpointId); + eventObserver.clearEvents(); + + // user has role 'http-upgrade-annotation' -> succeed + TestIdentityController.resetRoles().add("user", "user", "http-upgrade-annotation"); + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth(), httpUpgradeAnnotationEndpoint); + client.waitForMessages(1); + Assertions.assertEquals("ready", client.getMessages().get(0).toString()); + client.sendAndAwait("hello"); + client.waitForMessages(2); + Assertions.assertEquals("hello", client.getMessages().get(1).toString()); + } + assertEquals(1, eventObserver.authenticationSuccessEvents); + assertEquals(1, eventObserver.authenticationSuccessAsyncEvents); + assertEquals(0, eventObserver.authorizationFailureEvents); + assertEquals(0, eventObserver.authorizationFailureAsyncEvents); + assertEquals(1, eventObserver.authorizationSuccessEvents); + assertEquals(1, eventObserver.authorizationSuccessAsyncEvents); + assertEquals(0, eventObserver.authenticationFailureEvents); + assertEquals(0, eventObserver.authenticationFailureAsyncEvents); + assertAuthenticationSuccess(); + AuthorizationSuccessEvent authorizationSuccessEvent = assertAuthorizationSuccessEvent( + eventObserver.authorizationSuccessEvents); + String actualAuthZCtx = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT); + Assertions.assertNotNull(actualAuthZCtx); + Assertions.assertTrue(actualAuthZCtx.contains("RolesAllowed")); + httpServerRequest = (HttpServerRequest) authorizationSuccessEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.HTTP_REQUEST_KEY); + Assertions.assertNotNull(httpServerRequest); + authorizationSuccessEvent = assertAuthorizationSuccessEvent(eventObserver.authorizationSuccessAsyncEvents); + actualAuthZCtx = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT); + Assertions.assertNotNull(actualAuthZCtx); + Assertions.assertTrue(actualAuthZCtx.contains("RolesAllowed")); + httpServerRequest = (HttpServerRequest) authorizationSuccessEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.HTTP_REQUEST_KEY); + Assertions.assertNotNull(httpServerRequest); + endpointId = (String) authorizationSuccessEvent.getEventProperties() + .get(SecurityHttpUpgradeCheck.SECURED_ENDPOINT_ID_KEY); + Assertions.assertEquals("one-two-three", endpointId); + } + + @Test + public void testHttpUpgradeSecuredWithConfigurationEvents() { + // user is missing role 'http-upgrade-config' -> fail + TestIdentityController.resetRoles().add("user", "user"); + try (WSClient client = new WSClient(vertx)) { + CompletionException ce = assertThrows(CompletionException.class, + () -> client.connect(basicAuth(), httpUpgradeConfigEndpoint)); + Throwable root = ExceptionUtil.getRootCause(ce); + assertInstanceOf(UpgradeRejectedException.class, root); + assertTrue(root.getMessage().contains("403")); + } + assertEquals(1, eventObserver.authenticationSuccessEvents); + assertEquals(1, eventObserver.authenticationSuccessAsyncEvents); + assertEquals(1, eventObserver.authorizationFailureEvents); + assertEquals(1, eventObserver.authorizationFailureAsyncEvents); + assertEquals(0, eventObserver.authorizationSuccessEvents); + assertEquals(0, eventObserver.authorizationSuccessAsyncEvents); + assertEquals(0, eventObserver.authenticationFailureEvents); + assertEquals(0, eventObserver.authenticationFailureAsyncEvents); + assertAuthenticationSuccess(); + AuthorizationFailureEvent authorizationFailureEvent = assertAuthorizationFailureEvent( + eventObserver.authorizationFailureEvents); + RoutingContext routingContext = (RoutingContext) authorizationFailureEvent.getEventProperties() + .get(RoutingContext.class.getName()); + Assertions.assertNotNull(routingContext); + authorizationFailureEvent = assertAuthorizationFailureEvent(eventObserver.authorizationFailureAsyncEvents); + eventObserver.clearEvents(); + routingContext = (RoutingContext) authorizationFailureEvent.getEventProperties() + .get(RoutingContext.class.getName()); + Assertions.assertNotNull(routingContext); + + // user has role 'http-upgrade-config' -> succeed + TestIdentityController.resetRoles().add("user", "user", "http-upgrade-config"); + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth(), httpUpgradeConfigEndpoint); + client.waitForMessages(1); + Assertions.assertEquals("ready", client.getMessages().get(0).toString()); + client.sendAndAwait("hello"); + client.waitForMessages(2); + Assertions.assertEquals("hello", client.getMessages().get(1).toString()); + } + assertEquals(1, eventObserver.authenticationSuccessEvents); + assertEquals(1, eventObserver.authenticationSuccessAsyncEvents); + assertEquals(0, eventObserver.authorizationFailureEvents); + assertEquals(0, eventObserver.authorizationFailureAsyncEvents); + assertEquals(1, eventObserver.authorizationSuccessEvents); + assertEquals(1, eventObserver.authorizationSuccessAsyncEvents); + assertEquals(0, eventObserver.authenticationFailureEvents); + assertEquals(0, eventObserver.authenticationFailureAsyncEvents); + assertAuthenticationSuccess(); + AuthorizationSuccessEvent authorizationSuccessEvent = assertAuthorizationSuccessEvent( + eventObserver.authorizationSuccessEvents); + routingContext = (RoutingContext) authorizationSuccessEvent.getEventProperties() + .get(RoutingContext.class.getName()); + Assertions.assertNotNull(routingContext); + authorizationSuccessEvent = assertAuthorizationSuccessEvent(eventObserver.authorizationSuccessAsyncEvents); + routingContext = (RoutingContext) authorizationSuccessEvent.getEventProperties().get(RoutingContext.class.getName()); + Assertions.assertNotNull(routingContext); + } + + @Test + public void testOnTextMessageSecuredWithCheckerEvents() { + TestIdentityController.resetRoles().add("user", "user"); + try (WSClient client = new WSClient(vertx)) { + client.connect(basicAuth(), onTextMessageAnnotationEndpoint); + client.waitForMessages(1); + Assertions.assertEquals("ready", client.getMessages().get(0).toString()); + + // false -> permission checker denies access + client.sendAndAwait("false"); + client.waitForMessages(2); + Assertions.assertEquals("forbidden:user", client.getMessages().get(1).toString()); + + // true -> permission checker grants access + client.sendAndAwait("true"); + client.waitForMessages(3); + String response = client.getMessages().get(2).toString(); + assertTrue(Boolean.parseBoolean(response)); + } + + assertAuthenticationSuccess(); + assertEquals(1, eventObserver.authenticationSuccessEvents); + assertEquals(1, eventObserver.authenticationSuccessAsyncEvents); + assertEquals(0, eventObserver.authenticationFailureEvents); + assertEquals(0, eventObserver.authenticationFailureAsyncEvents); + // the first message was denied + assertEquals(1, eventObserver.authorizationFailureEvents); + assertEquals(1, eventObserver.authorizationFailureAsyncEvents); + // the second messages was permitted + assertEquals(1, eventObserver.authorizationSuccessEvents); + assertEquals(1, eventObserver.authorizationSuccessAsyncEvents); + + AuthorizationSuccessEvent authorizationSuccessEvent = assertAuthorizationSuccessEvent( + eventObserver.authorizationSuccessEvents); + String actualAuthZCtx = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT); + Assertions.assertNotNull(actualAuthZCtx); + Assertions.assertTrue(actualAuthZCtx.contains("PermissionSecurityCheck")); + RoutingContext routingContext = HttpSecurityUtils + .getRoutingContextAttribute(authorizationSuccessEvent.getSecurityIdentity()); + Assertions.assertNotNull(routingContext); + String securedMethod = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + Assertions.assertTrue(securedMethod.contains("OnTextMessageEndpoint#echo")); + authorizationSuccessEvent = assertAuthorizationSuccessEvent(eventObserver.authorizationSuccessAsyncEvents); + actualAuthZCtx = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.AUTHORIZATION_CONTEXT); + Assertions.assertNotNull(actualAuthZCtx); + Assertions.assertTrue(actualAuthZCtx.contains("PermissionSecurityCheck")); + routingContext = HttpSecurityUtils.getRoutingContextAttribute(authorizationSuccessEvent.getSecurityIdentity()); + Assertions.assertNotNull(routingContext); + securedMethod = (String) authorizationSuccessEvent.getEventProperties() + .get(AuthorizationSuccessEvent.SECURED_METHOD_KEY); + Assertions.assertTrue(securedMethod.contains("OnTextMessageEndpoint#echo")); + + AuthorizationFailureEvent authorizationFailureEvent = assertAuthorizationFailureEvent( + eventObserver.authorizationFailureEvents); + actualAuthZCtx = (String) authorizationFailureEvent.getEventProperties() + .get(AuthorizationFailureEvent.AUTHORIZATION_CONTEXT_KEY); + Assertions.assertNotNull(actualAuthZCtx); + Assertions.assertTrue(actualAuthZCtx.contains("PermissionSecurityCheck")); + routingContext = HttpSecurityUtils.getRoutingContextAttribute(authorizationFailureEvent.getSecurityIdentity()); + Assertions.assertNotNull(routingContext); + securedMethod = (String) authorizationFailureEvent.getEventProperties() + .get(AuthorizationFailureEvent.SECURED_METHOD_KEY); + Assertions.assertTrue(securedMethod.contains("OnTextMessageEndpoint#echo")); + assertAuthorizationFailureEvent(eventObserver.authorizationFailureAsyncEvents); + } + + private void assertAuthenticationSuccess() { + AuthenticationSuccessEvent authenticationSuccessEvent = eventObserver.authenticationSuccessEvents.get(0); + SecurityIdentity securityIdentity = authenticationSuccessEvent.getSecurityIdentity(); + Assertions.assertNotNull(securityIdentity); + Assertions.assertEquals("user", securityIdentity.getPrincipal().getName()); + RoutingContext routingContext = (RoutingContext) authenticationSuccessEvent.getEventProperties() + .get(RoutingContext.class.getName()); + Assertions.assertNotNull(routingContext); + } + + private static WebSocketConnectOptions basicAuth() { + return new WebSocketConnectOptions().addHeader(HttpHeaders.AUTHORIZATION.toString(), + new UsernamePasswordCredentials("user", "user").applyHttpChallenge(null).toHttpAuthorization()); + } + + private static void assertEquals(int size, List list) { + await().untilAsserted(() -> Assertions.assertEquals(size, list.size())); + } + + private static AuthorizationSuccessEvent assertAuthorizationSuccessEvent(List events) { + AuthorizationSuccessEvent event = events.get(0); + SecurityIdentity securityIdentity = event.getSecurityIdentity(); + Assertions.assertNotNull(securityIdentity); + Assertions.assertEquals("user", securityIdentity.getPrincipal().getName()); + return event; + } + + private static AuthorizationFailureEvent assertAuthorizationFailureEvent(List events) { + AuthorizationFailureEvent event = events.get(0); + SecurityIdentity securityIdentity = event.getSecurityIdentity(); + Assertions.assertNotNull(securityIdentity); + Assertions.assertEquals("user", securityIdentity.getPrincipal().getName()); + Throwable failure = event.getAuthorizationFailure(); + Assertions.assertInstanceOf(ForbiddenException.class, failure); + return event; + } + + @RolesAllowed("http-upgrade-annotation") + @WebSocket(path = "/http-upgrade-annotation-endpoint", endpointId = "one-two-three") + public static class HttpUpgradeAnnotationEndpoint { + + @OnOpen + String open() { + return "ready"; + } + + @OnTextMessage + String echo(String echo) { + return echo; + } + + } + + @WebSocket(path = "/http-upgrade-config-endpoint") + public static class HttpUpgradeConfigEndpoint { + + @OnOpen + String open() { + return "ready"; + } + + @OnTextMessage + String echo(String echo) { + return echo; + } + + } + + @WebSocket(path = "/on-text-message-endpoint") + public static class OnTextMessageEndpoint { + + @Inject + SecurityIdentity currentIdentity; + + @OnOpen + String open() { + return "ready"; + } + + @PermissionsAllowed("echo") + @OnTextMessage + String echo(boolean echo) { + return Boolean.toString(echo); + } + + @PermissionChecker("echo") + boolean canCallEcho(boolean echo) { + return echo; + } + + @OnError + String error(ForbiddenException t) { + return "forbidden:" + currentIdentity.getPrincipal().getName(); + } + } + + @Singleton + public static final class SecurityEventObserver { + + private final List authenticationSuccessEvents = new CopyOnWriteArrayList<>(); + private final List authenticationSuccessAsyncEvents = new CopyOnWriteArrayList<>(); + private final List authenticationFailureEvents = new CopyOnWriteArrayList<>(); + private final List authenticationFailureAsyncEvents = new CopyOnWriteArrayList<>(); + private final List authorizationSuccessEvents = new CopyOnWriteArrayList<>(); + private final List authorizationSuccessAsyncEvents = new CopyOnWriteArrayList<>(); + private final List authorizationFailureEvents = new CopyOnWriteArrayList<>(); + private final List authorizationFailureAsyncEvents = new CopyOnWriteArrayList<>(); + + private void clearEvents() { + authenticationSuccessEvents.clear(); + authenticationSuccessAsyncEvents.clear(); + authenticationFailureEvents.clear(); + authenticationFailureAsyncEvents.clear(); + authorizationSuccessEvents.clear(); + authorizationSuccessAsyncEvents.clear(); + authorizationFailureEvents.clear(); + authorizationFailureAsyncEvents.clear(); + } + + void observeAuthenticationSuccess(@Observes AuthenticationSuccessEvent event) { + authenticationSuccessEvents.add(event); + } + + void observeAuthenticationSuccessAsync(@ObservesAsync AuthenticationSuccessEvent event) { + authenticationSuccessAsyncEvents.add(event); + } + + void observeAuthenticationFailure(@Observes AuthenticationFailureEvent event) { + authenticationFailureEvents.add(event); + } + + void observeAuthenticationFailureAsync(@ObservesAsync AuthenticationFailureEvent event) { + authenticationFailureAsyncEvents.add(event); + } + + void observeAuthorizationSuccess(@Observes AuthorizationSuccessEvent event) { + authorizationSuccessEvents.add(event); + } + + void observeAuthorizationSuccessAsync(@ObservesAsync AuthorizationSuccessEvent event) { + authorizationSuccessAsyncEvents.add(event); + } + + void observeAuthorizationFailure(@Observes AuthorizationFailureEvent event) { + authorizationFailureEvents.add(event); + } + + void observeAuthorizationFailureAsync(@ObservesAsync AuthorizationFailureEvent event) { + authorizationFailureAsyncEvents.add(event); + } + + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecurityHttpUpgradeCheck.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecurityHttpUpgradeCheck.java index 91f50a62c9406..af56449d3fa98 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecurityHttpUpgradeCheck.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecurityHttpUpgradeCheck.java @@ -7,29 +7,40 @@ import java.util.Map; import io.quarkus.security.ForbiddenException; +import io.quarkus.security.identity.SecurityIdentity; +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.SecurityEventHelper; import io.quarkus.websockets.next.HttpUpgradeCheck; import io.smallrye.mutiny.Uni; -public class SecurityHttpUpgradeCheck implements HttpUpgradeCheck { +public final class SecurityHttpUpgradeCheck implements HttpUpgradeCheck { public static final int BEAN_PRIORITY = Integer.MAX_VALUE - 100; + public static final String SECURED_ENDPOINT_ID_KEY = SecurityHttpUpgradeCheck.class.getName() + ".ENDPOINT_ID"; + public static final String HTTP_REQUEST_KEY = SecurityHttpUpgradeCheck.class.getName() + ".HTTP_REQUEST"; private final String redirectUrl; private final Map endpointToCheck; + private final SecurityEventHelper securityEventHelper; - SecurityHttpUpgradeCheck(String redirectUrl, Map endpointToCheck) { + SecurityHttpUpgradeCheck(String redirectUrl, Map endpointToCheck, + SecurityEventHelper securityEventHelper) { this.redirectUrl = redirectUrl; this.endpointToCheck = Map.copyOf(endpointToCheck); + this.securityEventHelper = securityEventHelper; } @Override public Uni perform(HttpUpgradeContext context) { - return context.securityIdentity().chain(identity -> endpointToCheck.get(context.endpointId()) + final SecurityCheck securityCheck = endpointToCheck.get(context.endpointId()); + return context.securityIdentity().chain(identity -> securityCheck .nonBlockingApply(identity, (MethodDescription) null, null) - .replaceWith(CheckResult::permitUpgradeSync) - .onFailure(SecurityException.class).recoverWithItem(this::rejectUpgrade)); + .replaceWith(() -> permitUpgrade(identity, securityCheck, context)) + .onFailure(SecurityException.class) + .recoverWithItem(t -> rejectUpgrade(t, identity, securityCheck, context))); } @Override @@ -37,7 +48,24 @@ public boolean appliesTo(String endpointId) { return endpointToCheck.containsKey(endpointId); } - private CheckResult rejectUpgrade(Throwable throwable) { + private CheckResult permitUpgrade(SecurityIdentity identity, SecurityCheck securityCheck, HttpUpgradeContext context) { + if (securityEventHelper.fireEventOnSuccess()) { + String authorizationContext = securityCheck.getClass().getName(); + AuthorizationSuccessEvent successEvent = new AuthorizationSuccessEvent(identity, authorizationContext, + Map.of(SECURED_ENDPOINT_ID_KEY, context.endpointId(), HTTP_REQUEST_KEY, context.httpRequest())); + securityEventHelper.fireSuccessEvent(successEvent); + } + return CheckResult.permitUpgradeSync(); + } + + private CheckResult rejectUpgrade(Throwable throwable, SecurityIdentity identity, SecurityCheck securityCheck, + HttpUpgradeContext context) { + if (securityEventHelper.fireEventOnFailure()) { + String authorizationContext = securityCheck.getClass().getName(); + AuthorizationFailureEvent failureEvent = new AuthorizationFailureEvent(identity, throwable, authorizationContext, + Map.of(SECURED_ENDPOINT_ID_KEY, context.endpointId(), HTTP_REQUEST_KEY, context.httpRequest())); + securityEventHelper.fireFailureEvent(failureEvent); + } if (redirectUrl != null) { return CheckResult.rejectUpgradeSync(302, Map.of(LOCATION.toString(), List.of(redirectUrl), diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index ddee52c2ad774..7ecd861af2196 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -1,20 +1,29 @@ package io.quarkus.websockets.next.runtime; +import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE; +import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS; + import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.util.TypeLiteral; +import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.spi.runtime.SecurityCheck; +import io.quarkus.security.spi.runtime.SecurityEventHelper; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.quarkus.websockets.next.HandshakeRequest; @@ -189,11 +198,19 @@ SecuritySupport initializeSecuritySupport(ArcContainer container, RoutingContext return SecuritySupport.NOOP; } - public Supplier createSecurityHttpUpgradeCheck(Map endpointToCheck) { - return new Supplier() { + public Function, HttpUpgradeCheck> createSecurityHttpUpgradeCheck( + Map endpointToCheck) { + return new Function, HttpUpgradeCheck>() { @Override - public HttpUpgradeCheck get() { - return new SecurityHttpUpgradeCheck(config.security().authFailureRedirectUrl().orElse(null), endpointToCheck); + public HttpUpgradeCheck apply(SyntheticCreationalContext ctx) { + boolean securityEventsEnabled = ConfigProvider.getConfig().getValue("quarkus.security.events.enabled", + Boolean.class); + var securityEventHelper = new SecurityEventHelper<>(ctx.getInjectedReference(new TypeLiteral<>() { + }), ctx.getInjectedReference(new TypeLiteral<>() { + }), AUTHORIZATION_SUCCESS, + AUTHORIZATION_FAILURE, ctx.getInjectedReference(BeanManager.class), securityEventsEnabled); + return new SecurityHttpUpgradeCheck(config.security().authFailureRedirectUrl().orElse(null), endpointToCheck, + securityEventHelper); } }; }