From 61cdd8851659792e24a0bdda8a913af271d82b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sat, 23 Nov 2024 15:13:13 +0100 Subject: [PATCH] fix(security,kotlin) Support @AuthorizationPolicy on suspended endpoints --- .../deployment/HttpSecurityProcessor.java | 10 +++- .../resteasy-reactive-kotlin/standard/pom.xml | 17 +++++++ .../reactive/kotlin/SecuredClassResource.kt | 17 +++++++ .../reactive/kotlin/SecuredMethodResource.kt | 14 ++++++ .../kotlin/SuspendAuthorizationPolicy.kt | 26 ++++++++++ .../resteasy/reactive/kotlin/SecurityTest.kt | 47 +++++++++++++++++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt 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 80a2f6f2ee832..51829fb5db14e 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 @@ -95,6 +95,7 @@ public class HttpSecurityProcessor { private static final DotName AUTH_MECHANISM_NAME = DotName.createSimple(HttpAuthenticationMechanism.class); private static final DotName BASIC_AUTH_MECH_NAME = DotName.createSimple(BasicAuthenticationMechanism.class); private static final DotName BASIC_AUTH_ANNOTATION_NAME = DotName.createSimple(BasicAuthentication.class); + private static final String KOTLIN_SUSPEND_IMPL_SUFFIX = "$suspendImpl"; @Record(ExecutionTime.STATIC_INIT) @BuildStep @@ -576,9 +577,16 @@ private static Stream getPolicyTargetEndpointCandidates(AnnotationTa if (target.kind() == AnnotationTarget.Kind.METHOD) { var method = target.asMethod(); if (!hasProperEndpointModifiers(method)) { + if (method.isSynthetic() && method.name().endsWith(KOTLIN_SUSPEND_IMPL_SUFFIX)) { + // ATM there are 2 methods for Kotlin endpoint like this: + // @AuthorizationPolicy(name = "suspended") + // suspend fun sayHi() = "Hi" + // the synthetic method doesn't need to be secured, but it keeps security annotations + return Stream.empty(); + } throw new RuntimeException(""" Found method annotated with the @AuthorizationPolicy annotation that is not an endpoint: %s#%s - """.formatted(method.asClass().name().toString(), method.name())); + """.formatted(method.declaringClass().name().toString(), method.name())); } return Stream.of(method); } diff --git a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml index b20e5a1f5192f..d37324da463ac 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml +++ b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml @@ -39,6 +39,10 @@ io.quarkus quarkus-kotlin + + io.quarkus + quarkus-security + io.quarkus quarkus-integration-test-shared-library @@ -186,6 +190,19 @@ + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt new file mode 100644 index 0000000000000..214bfc4cf8cd1 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredClassResource.kt @@ -0,0 +1,17 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.vertx.http.security.AuthorizationPolicy +import jakarta.annotation.security.PermitAll +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +@AuthorizationPolicy(name = "suspended") +@Path("/secured-class") +class SecuredClassResource { + + @Path("/authorization-policy-suspend") + @GET + suspend fun authorizationPolicySuspend() = "Hello from Quarkus REST" + + @PermitAll @Path("/public") @GET suspend fun publicEndpoint() = "Hello to everyone!" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt new file mode 100644 index 0000000000000..f2d9134894dce --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecuredMethodResource.kt @@ -0,0 +1,14 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.vertx.http.security.AuthorizationPolicy +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +@Path("/secured-method") +class SecuredMethodResource { + + @Path("/authorization-policy-suspend") + @GET + @AuthorizationPolicy(name = "suspended") + suspend fun authorizationPolicySuspend() = "Hello from Quarkus REST" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt new file mode 100644 index 0000000000000..9d54e64bed799 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SuspendAuthorizationPolicy.kt @@ -0,0 +1,26 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.security.identity.SecurityIdentity +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult +import io.smallrye.mutiny.Uni +import io.vertx.core.http.HttpHeaders +import io.vertx.ext.web.RoutingContext +import jakarta.enterprise.context.ApplicationScoped + +@ApplicationScoped +class SuspendAuthorizationPolicy : HttpSecurityPolicy { + override fun checkPermission( + request: RoutingContext?, + identity: Uni?, + requestContext: HttpSecurityPolicy.AuthorizationRequestContext? + ): Uni { + val authZHeader = request?.request()?.getHeader(HttpHeaders.AUTHORIZATION) + if (authZHeader == "you-can-trust-me") { + return CheckResult.permit() + } + return CheckResult.deny() + } + + override fun name() = "suspended" +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt new file mode 100644 index 0000000000000..a8b81532d5dbe --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SecurityTest.kt @@ -0,0 +1,47 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import io.vertx.core.http.HttpHeaders +import org.hamcrest.CoreMatchers +import org.junit.jupiter.api.Test + +@QuarkusTest +class SecurityTest { + + @Test + fun testAuthorizationPolicyOnSuspendedMethod_MethodLevel() { + When { get("/secured-method/authorization-policy-suspend") } Then { statusCode(403) } + Given { header(HttpHeaders.AUTHORIZATION.toString(), "you-can-trust-me") } When + { + get("/secured-method/authorization-policy-suspend") + } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello from Quarkus REST")) + } + } + + @Test + fun testAuthorizationPolicyOnSuspendedMethod_ClassLevel() { + // test class-level annotation is applied on a secured method + When { get("/secured-class/authorization-policy-suspend") } Then { statusCode(403) } + Given { header(HttpHeaders.AUTHORIZATION.toString(), "you-can-trust-me") } When + { + get("/secured-class/authorization-policy-suspend") + } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello from Quarkus REST")) + } + + // test method-level @PermitAll has priority over @AuthorizationPolicy on the class + When { get("/secured-class/public") } Then + { + statusCode(200) + body(CoreMatchers.`is`("Hello to everyone!")) + } + } +}