diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityPolicy.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityPolicy.java new file mode 100644 index 0000000000000..b75e8e110470b --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityPolicy.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ResourceInfo; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomHttpSecurityPolicy implements HttpSecurityPolicy { + + @Inject + ResourceInfo resourceInfo; + + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if ("CustomPolicyResource".equals(resourceInfo.getResourceClass().getSimpleName()) + && "isUserAdmin".equals(resourceInfo.getResourceMethod().getName())) { + return identity.onItem().ifNotNull().transform(i -> { + if (i.hasRole("user")) { + return new CheckResult(true, QuarkusSecurityIdentity.builder(i).addRole("admin").build()); + } + return CheckResult.PERMIT; + }); + } + return Uni.createFrom().item(CheckResult.PERMIT); + } + + @Override + public String name() { + return "custom"; + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java new file mode 100644 index 0000000000000..8ed2e3cba77d1 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java @@ -0,0 +1,55 @@ +package io.quarkus.resteasy.test.security; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class CustomHttpSecurityWithJaxRsSecurityContextTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(CustomPolicyResource.class, TestIdentityProvider.class, + TestIdentityController.class, CustomHttpSecurityPolicy.class) + .addAsResource(new StringAsset(""" + quarkus.http.auth.permission.custom-policy-1.paths=/custom-policy/is-admin + quarkus.http.auth.permission.custom-policy-1.policy=custom + quarkus.http.auth.permission.custom-policy-1.applies-to=JAXRS + """), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("test", "test", "test") + .add("user", "user", "user"); + } + + @Test + public void testAugmentedIdentityInSecurityContext() { + // test that custom HTTP Security Policy is applied, it added 'admin' role to the 'user' + // and this new role is present in the JAX-RS SecurityContext + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/custom-policy/is-admin") + .then() + .statusCode(200) + .body(Matchers.is("true")); + RestAssured + .given() + .auth().preemptive().basic("test", "test") + .get("/custom-policy/is-admin") + .then() + .statusCode(200) + .body(Matchers.is("false")); + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomPolicyResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomPolicyResource.java new file mode 100644 index 0000000000000..cd29c214bac9b --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/CustomPolicyResource.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.test.security; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; + +@Path("custom-policy") +public class CustomPolicyResource { + + @Path("is-admin") + @GET + public boolean isUserAdmin(@Context SecurityContext context) { + return context.isUserInRole("admin"); + } + +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index b37aa04e3c217..8fbaa8e3f20db 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -1504,35 +1504,33 @@ MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, Combine final boolean applySecurityInterceptors = !eagerSecurityInterceptors.isEmpty(); final var interceptedMethods = applySecurityInterceptors ? collectInterceptedMethods(eagerSecurityInterceptors) : null; - final boolean denyJaxRs = securityConfig.denyJaxRs(); - final boolean hasDefaultJaxRsRolesAllowed = !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty(); + final boolean withDefaultSecurityCheck = securityConfig.denyJaxRs() + || !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty(); var index = indexBuildItem.getComputingIndex(); return new MethodScannerBuildItem(new MethodScanner() { @Override public List scan(MethodInfo method, ClassInfo actualEndpointClass, Map methodContext) { if (applySecurityInterceptors && interceptedMethods.contains(method)) { - // EagerSecurityHandler needs to be present whenever the method requires eager interceptor - // because JAX-RS specific HTTP Security policies are defined by runtime config properties - // for example: when you annotate resource method with @Tenant("hr") you select OIDC tenant, - // so we can't authenticate before the tenant is selected, only after then HTTP perms can be checked return List.of(EagerSecurityInterceptorHandler.Customizer.newInstance(), - EagerSecurityHandler.Customizer.newInstance()); + EagerSecurityHandler.Customizer.newInstance(false)); } else { - if (denyJaxRs || hasDefaultJaxRsRolesAllowed) { - return List.of(EagerSecurityHandler.Customizer.newInstance()); - } else { - return Objects - .requireNonNullElse( - consumeStandardSecurityAnnotations(method, actualEndpointClass, index, - (c) -> List.of(EagerSecurityHandler.Customizer.newInstance())), - Collections.emptyList()); - } + return List.of(newEagerSecurityHandlerCustomizerInstance(method, actualEndpointClass, index, + withDefaultSecurityCheck)); } } }); } + private HandlerChainCustomizer newEagerSecurityHandlerCustomizerInstance(MethodInfo method, ClassInfo actualEndpointClass, + IndexView index, boolean withDefaultSecurityCheck) { + if (withDefaultSecurityCheck + || consumeStandardSecurityAnnotations(method, actualEndpointClass, index, (c) -> c) != null) { + return EagerSecurityHandler.Customizer.newInstance(false); + } + return EagerSecurityHandler.Customizer.newInstance(true); + } + /** * This results in adding {@link AllWriteableMarker} to user provided {@link MessageBodyWriter} classes * that handle every class diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityPolicy.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityPolicy.java new file mode 100644 index 0000000000000..9392bd47e819c --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityPolicy.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ResourceInfo; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomHttpSecurityPolicy implements HttpSecurityPolicy { + + @Inject + ResourceInfo resourceInfo; + + @Override + public Uni checkPermission(RoutingContext request, Uni identity, + AuthorizationRequestContext requestContext) { + if ("CustomPolicyResource".equals(resourceInfo.getResourceClass().getSimpleName()) + && "isUserAdmin".equals(resourceInfo.getResourceMethod().getName())) { + return identity.onItem().ifNotNull().transform(i -> { + if (i.hasRole("user")) { + return new CheckResult(true, QuarkusSecurityIdentity.builder(i).addRole("admin").build()); + } + return CheckResult.PERMIT; + }); + } + return Uni.createFrom().item(CheckResult.PERMIT); + } + + @Override + public String name() { + return "custom"; + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java new file mode 100644 index 0000000000000..cdf6459f0cbc7 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomHttpSecurityWithJaxRsSecurityContextTest.java @@ -0,0 +1,55 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class CustomHttpSecurityWithJaxRsSecurityContextTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(CustomPolicyResource.class, TestIdentityProvider.class, + TestIdentityController.class, CustomHttpSecurityPolicy.class) + .addAsResource(new StringAsset(""" + quarkus.http.auth.permission.custom-policy-1.paths=/custom-policy/is-admin + quarkus.http.auth.permission.custom-policy-1.policy=custom + quarkus.http.auth.permission.custom-policy-1.applies-to=JAXRS + """), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("test", "test", "test") + .add("user", "user", "user"); + } + + @Test + public void testAugmentedIdentityInSecurityContext() { + // test that custom HTTP Security Policy is applied, it added 'admin' role to the 'user' + // and this new role is present in the JAX-RS SecurityContext + RestAssured + .given() + .auth().preemptive().basic("user", "user") + .get("/custom-policy/is-admin") + .then() + .statusCode(200) + .body(Matchers.is("true")); + RestAssured + .given() + .auth().preemptive().basic("test", "test") + .get("/custom-policy/is-admin") + .then() + .statusCode(200) + .body(Matchers.is("false")); + } + +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomPolicyResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomPolicyResource.java new file mode 100644 index 0000000000000..851b7c60d124a --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/CustomPolicyResource.java @@ -0,0 +1,16 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.SecurityContext; + +@Path("custom-policy") +public class CustomPolicyResource { + + @Path("is-admin") + @GET + public boolean isUserAdmin(SecurityContext context) { + return context.isUserInRole("admin"); + } + +} 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 65d62e24c62f9..59b74d611bcb5 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 @@ -9,6 +9,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import org.jboss.resteasy.reactive.common.model.ResourceClass; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; @@ -40,8 +41,13 @@ public void apply(SecurityIdentity identity, MethodDescription method, Object[] } }; + private final boolean onlyCheckForHttpPermissions; private volatile SecurityCheck check; + public EagerSecurityHandler(boolean onlyCheckForHttpPermissions) { + this.onlyCheckForHttpPermissions = onlyCheckForHttpPermissions; + } + @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { if (!EagerSecurityContext.instance.authorizationController.isAuthorizationEnabled()) { @@ -56,7 +62,12 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti return; } else { // only permission check - check = EagerSecurityContext.instance.getPermissionCheck(requestContext, null); + check = Uni.createFrom().deferred(new Supplier>() { + @Override + public Uni get() { + return EagerSecurityContext.instance.getPermissionCheck(requestContext, null); + } + }); } } else { if (EagerSecurityContext.instance.doNotRunPermissionSecurityCheck) { @@ -96,7 +107,7 @@ public void onFailure(Throwable failure) { } private Function> getSecurityCheck(ResteasyReactiveRequestContext requestContext) { - if (this.check == NULL_SENTINEL) { + if (this.onlyCheckForHttpPermissions || this.check == NULL_SENTINEL) { return null; } SecurityCheck check = this.check; @@ -210,20 +221,39 @@ private static boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext re return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null; } - public static class Customizer implements HandlerChainCustomizer { + public static abstract class Customizer implements HandlerChainCustomizer { - public static HandlerChainCustomizer newInstance() { - return new Customizer(); + public static HandlerChainCustomizer newInstance(boolean onlyCheckForHttpPermissions) { + return onlyCheckForHttpPermissions ? new HttpPermissionsOnlyCustomizer() + : new HttpPermissionsAndSecurityChecksCustomizer(); } @Override public List handlers(Phase phase, ResourceClass resourceClass, ServerResourceMethod serverResourceMethod) { if (phase == Phase.AFTER_MATCH) { - return Collections.singletonList(new EagerSecurityHandler()); + return Collections.singletonList(new EagerSecurityHandler(onlyCheckForHttpPermissions())); } return Collections.emptyList(); } + protected abstract boolean onlyCheckForHttpPermissions(); + + public static final class HttpPermissionsOnlyCustomizer extends Customizer { + + @Override + protected boolean onlyCheckForHttpPermissions() { + return true; + } + } + + public static final class HttpPermissionsAndSecurityChecksCustomizer extends Customizer { + + @Override + protected boolean onlyCheckForHttpPermissions() { + return false; + } + } + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 21fbdbb326576..3e801f90c0032 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -43,7 +43,7 @@ public class AbstractPathMatchingHttpSecurityPolicy { public AbstractPathMatchingHttpSecurityPolicy(Map permissions, Map rolePolicy, String rootPath, Instance installedPolicies, PolicyMappingConfig.AppliesTo appliesTo) { - boolean hasNoPermissions = permissions.isEmpty(); + boolean hasNoPermissions = true; var namedHttpSecurityPolicies = toNamedHttpSecPolicies(rolePolicy, installedPolicies); List>> sharedPermsMatchers = new ArrayList<>(); final var builder = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll)