From 17a8b2e220d89e2f6fe3dbad0fd3730664aaf720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 19 Nov 2024 13:18:17 +0100 Subject: [PATCH] Make sure identity produced by `@TestSecurity` is augmented (cherry picked from commit e6a895b76b6f7c0faf965beebb42e3ea5faa97e3) --- docs/src/main/asciidoc/security-testing.adoc | 44 +++++++++++++++++ .../deployment/SecurityProcessor.java | 4 +- ...usPermissionSecurityIdentityAugmentor.java | 2 +- .../runtime/SecurityCheckRecorder.java | 8 ++-- .../it/resteasy/elytron/RootResource.java | 17 +++++++ .../elytron/TestSecurityTestCase.java | 22 +++++++++ .../quarkus/it/keycloak/CustomPermission.java | 10 ++++ .../it/keycloak/ProtectedJwtResource.java | 9 ++++ .../TestSecurityIdentityAugmentor.java | 37 ++++++++++++++ .../src/main/resources/application.properties | 2 + .../it/keycloak/TestSecurityLazyAuthTest.java | 48 +++++++++++++++++++ ...stractTestHttpAuthenticationMechanism.java | 34 ++++++++++++- .../QuarkusSecurityTestExtension.java | 48 ++++++++++++++++--- .../security/TestIdentityAssociation.java | 4 +- .../quarkus/test/security/TestSecurity.java | 12 ++++- 15 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index bed66779c75e3..f89af3f3c4af6 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -156,6 +156,50 @@ public String getDetail() { } ---- +It is also possible to set custom permissions like in the example below: + +[source,java] +---- +@PermissionsAllowed("see", permission = CustomPermission.class) +public String getDetail() { + return "detail"; +} +---- + +The `CustomPermission` needs to be granted to the `SecurityIdentity` created +by the `@TestSecurity` annotation with a `SecurityIdentityAugmentor` CDI bean: + +[source,java] +---- +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + final SecurityIdentity augmentedIdentity; + if (shouldGrantCustomPermission(securityIdentity) { + augmentedIdentity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("see")).build(); + } else { + augmentedIdentity = securityIdentity; + } + return Uni.createFrom().item(augmentedIdentity); + } +} +---- + +Quarkus will only augment the `SecurityIdentity` created with the `@TestSecurity` annotation if you set +the `@TestSecurity#augmentors` annotation attribute to the `CustomSecurityIdentityAugmentor.class` like this: + +[source,java] +---- +@Test +@TestSecurity(user = "testUser", permissions = "see:detail", augmentors = CustomSecurityIdentityAugmentor.class) +void someTestMethod() { + ... +} +---- + === Mixing security tests If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth 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 82fc4151c7ede..bd5b9bcf607e4 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 @@ -107,6 +107,7 @@ import io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder; import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.IdentityProviderManagerCreator; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder; import io.quarkus.security.runtime.SecurityBuildTimeConfig; import io.quarkus.security.runtime.SecurityCheckRecorder; @@ -691,7 +692,8 @@ void configurePermissionCheckers(PermissionSecurityChecksBuilderBuildItem checke // - this processor relies on the bean archive index (cycle: idx -> additional bean -> idx) // - we have injection points (=> better validation from Arc) as checker beans are only requested from this augmentor var syntheticBeanConfigurator = SyntheticBeanBuildItem - .configure(SecurityIdentityAugmentor.class) + .configure(QuarkusPermissionSecurityIdentityAugmentor.class) + .addType(SecurityIdentityAugmentor.class) // ATM we do get augmentors from CDI once, no need to keep the instance in the CDI container .scope(Dependent.class) .unremovable() diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java index 300cd5e830204..7ca713dc4c4bd 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java @@ -15,7 +15,7 @@ * Adds a permission checker that grants access to the {@link QuarkusPermission} * when {@link QuarkusPermission#isGranted(SecurityIdentity)} is true. */ -final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { +public final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { /** * Permission checker only authorizes authenticated users and checkers shouldn't throw a security exception. 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 6667ba40b89ca..ad1e9ba9ca8a9 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 @@ -29,7 +29,6 @@ import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.ForbiddenException; import io.quarkus.security.StringPermission; -import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; @@ -436,10 +435,11 @@ private static Object convertMethodParamToPermParam(int i, Object methodArg, } } - public Function, SecurityIdentityAugmentor> createPermissionAugmentor() { - return new Function, SecurityIdentityAugmentor>() { + public Function, QuarkusPermissionSecurityIdentityAugmentor> createPermissionAugmentor() { + return new Function, QuarkusPermissionSecurityIdentityAugmentor>() { @Override - public SecurityIdentityAugmentor apply(SyntheticCreationalContext ctx) { + public QuarkusPermissionSecurityIdentityAugmentor apply( + SyntheticCreationalContext ctx) { return new QuarkusPermissionSecurityIdentityAugmentor(ctx.getInjectedReference(BlockingSecurityExecutor.class)); } }; diff --git a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java index 11e6a1d3675b7..a8ff4305d70c9 100644 --- a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java +++ b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java @@ -16,6 +16,8 @@ import jakarta.ws.rs.core.SecurityContext; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/") @@ -73,4 +75,19 @@ public String getAttributes() { .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining(",")); } + + @GET + @Path("/test-security-permission-checker") + @PermissionsAllowed("see-principal") + public String getPrincipal(@Context SecurityContext sec) { + return sec.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName(); + } + + @PermissionChecker("see-principal") + boolean canSeePrincipal(SecurityContext sec) { + if (sec.getUserPrincipal() == null || sec.getUserPrincipal().getName() == null) { + return false; + } + return "meat loaf".equals(sec.getUserPrincipal().getName()); + } } diff --git a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java index 25017f7014fc4..43158fc4d374c 100644 --- a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java +++ b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java @@ -11,6 +11,7 @@ import jakarta.inject.Inject; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -23,6 +24,7 @@ import io.quarkus.test.security.AttributeType; import io.quarkus.test.security.SecurityAttribute; import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; @QuarkusTest class TestSecurityTestCase { @@ -187,4 +189,24 @@ static Stream arrayParams() { arguments(new int[] { 1, 2 }, new String[] { "hello", "world" })); } + @Test + public void testPermissionChecker_anonymousUser() { + // user is not authenticated and access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(401); + } + + @Test + @TestSecurity(user = "authenticated-user") + public void testPermissionChecker_authenticatedUser() { + // user is authenticated, but access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(403); + } + + @Test + @TestSecurity(user = "meat loaf") + public void testPermissionChecker_authorizedUser() { + // user is authenticated and access should be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(200) + .body(Matchers.is("meat loaf:meat loaf:meat loaf")); + } } diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java new file mode 100644 index 0000000000000..5325e49879a92 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java @@ -0,0 +1,10 @@ +package io.quarkus.it.keycloak; + +import java.security.BasicPermission; + +public class CustomPermission extends BasicPermission { + + public CustomPermission(String name) { + super(name); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java index d9187dc7e9d1f..fdfbe0128baee 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java @@ -14,6 +14,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/web-app") @@ -40,6 +41,14 @@ public String testSecurity() { + principal.getName(); } + @GET + @Path("test-security-with-augmentors") + @PermissionsAllowed(permission = CustomPermission.class, value = "augmented") + public String testSecurityWithAugmentors() { + return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + + principal.getName(); + } + @POST @Path("test-security") @Consumes("application/json") diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..6e8662823ad16 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java @@ -0,0 +1,37 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class TestSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + private static volatile boolean invoked = false; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + invoked = true; + final SecurityIdentity identity; + if (securityIdentity.isAnonymous() || !"authorized-user".equals(securityIdentity.getPrincipal().getName())) { + identity = securityIdentity; + } else { + identity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("augmented")).build(); + } + return Uni.createFrom().item(identity); + } + + public static boolean isInvoked() { + return invoked; + } + + public static void resetInvoked() { + invoked = false; + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties index fa4f560b45c04..f843b520dc74e 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -1,3 +1,5 @@ +quarkus.keycloak.devservices.enabled=false + mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.path.groups=realm_access/roles diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 45812da96c15a..a573cb8e6af55 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -7,6 +7,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -21,6 +22,53 @@ @TestHTTPEndpoint(ProtectedJwtResource.class) public class TestSecurityLazyAuthTest { + @Test + public void testTestSecurityAnnotationWithAugmentors_anonymousUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is not authenticated and doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(401); + // identity manager applies augmentors on anonymous identity + // because @TestSecurity is not in action and that's what we do for the anonymous requests + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + // and no augmentors are applied + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authorizedUser() { + // should fail because no augmentors are applied + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authorizedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(200) + .body(is("authorized-user:authorized-user:authorized-user")); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + @Test @TestAsUser1Viewer public void testWithDummyUser() { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java index 1359b83e041f7..732b7623d79dd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java @@ -1,15 +1,24 @@ package io.quarkus.test.security; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.ROUTING_CONTEXT_ATTRIBUTE; + import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import jakarta.annotation.PostConstruct; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; @@ -21,7 +30,11 @@ abstract class AbstractTestHttpAuthenticationMechanism implements HttpAuthentica @Inject TestIdentityAssociation testIdentityAssociation; + @Inject + BlockingSecurityExecutor blockingSecurityExecutor; + protected volatile String authMechanism = null; + protected volatile List> augmentors = null; @PostConstruct public void check() { @@ -32,8 +45,21 @@ public void check() { } @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - return Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + public Uni authenticate(RoutingContext event, IdentityProviderManager identityProviderManager) { + var identity = Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + if (augmentors != null && testIdentityAssociation.getTestIdentity() != null) { + var requestContext = new AuthenticationRequestContext() { + @Override + public Uni runBlocking(Supplier supplier) { + return blockingSecurityExecutor.executeBlocking(supplier); + } + }; + var requestAttributes = Map. of(ROUTING_CONTEXT_ATTRIBUTE, event); + for (var augmentor : augmentors) { + identity = identity.flatMap(i -> augmentor.get().augment(i, requestContext, requestAttributes)); + } + } + return identity; } @Override @@ -55,4 +81,8 @@ public Uni getCredentialTransport(RoutingContext contex void setAuthMechanism(String authMechanism) { this.authMechanism = authMechanism; } + + void setSecurityIdentityAugmentors(List> augmentors) { + this.augmentors = augmentors; + } } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java index ef556bad11fc8..fc6ac4d24f67d 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -5,9 +5,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.security.Permission; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,8 +19,12 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; @@ -34,11 +40,13 @@ public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallba public void afterEach(QuarkusTestMethodContext context) { try { if (getAnnotationContainer(context).isPresent()) { - CDI.current().select(TestAuthController.class).get().setEnabled(true); - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(true); + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(null); + testMechanism.setSecurityIdentityAugmentors(null); } - var testIdentity = CDI.current().select(TestIdentityAssociation.class).get(); + var testIdentity = container.select(TestIdentityAssociation.class).get(); testIdentity.setTestIdentity(null); testIdentity.setPathBasedIdentity(false); } @@ -59,7 +67,8 @@ public void beforeEach(QuarkusTestMethodContext context) { var annotationContainer = annotationContainerOptional.get(); Annotation[] allAnnotations = annotationContainer.getElement().getAnnotations(); TestSecurity testSecurity = annotationContainer.getAnnotation(); - CDI.current().select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); if (testSecurity.user().isEmpty()) { if (testSecurity.roles().length != 0) { throw new RuntimeException("Cannot specify roles without a username in @TestSecurity"); @@ -82,12 +91,37 @@ public void beforeEach(QuarkusTestMethodContext context) { } SecurityIdentity userIdentity = augment(user.build(), allAnnotations); - CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); + container.select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); if (!testSecurity.authMechanism().isEmpty()) { - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(testSecurity.authMechanism()); } - CDI.current().select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + container.select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + } + + // run SecurityIdentityAugmentors when: + List> augmentors = new ArrayList<>(); + // 1. user opted-in with @TestSecurity#augmentors, run augmentors listed by user + for (Class augmentorClass : testSecurity.augmentors()) { + var augmentorInstance = container.select(augmentorClass); + if (!augmentorInstance.isResolvable()) { + var testMethodName = context.getTestMethod() == null ? "" : context.getTestMethod().getName(); + throw new RuntimeException(""" + SecurityIdentityAugmentor class '%s' specified with '@TestSecurity#augmentors' annotation + attribute on method '%s' is not available as a CDI bean. + """.formatted(augmentorClass, testMethodName)); + } + augmentors.add(augmentorInstance); + } + // 2. @PermissionChecker is used, run the augmentor that enables this functionality + var quarkusPermissionAugmentor = container.select(QuarkusPermissionSecurityIdentityAugmentor.class); + if (quarkusPermissionAugmentor.isResolvable()) { + augmentors.add(quarkusPermissionAugmentor); + } + if (!augmentors.isEmpty()) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { + testMechanism.setSecurityIdentityAugmentors(augmentors); + } } } } catch (Exception e) { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java index 569a9f266e880..807209452d474 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java @@ -26,12 +26,12 @@ public void check() { } } - volatile SecurityIdentity testIdentity; + private volatile SecurityIdentity testIdentity; /** * Whether authentication is successful only if right mechanism was used to authenticate. */ - volatile boolean isPathBasedIdentity = false; + private volatile boolean isPathBasedIdentity = false; /** * A request scoped delegate that allows the system to function as normal when diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java index 36543039989e4..a0fc93a1725fd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java @@ -8,6 +8,7 @@ import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @@ -35,11 +36,18 @@ * That is, permission is separated from actions with {@link PermissionsAllowed#PERMISSION_TO_ACTION_SEPARATOR}. * For example, value {@code see:detail} gives permission to {@code see} action {@code detail}. * All permissions are added as {@link io.quarkus.security.StringPermission}. - * If you need to test custom permissions, you can add them with - * {@link io.quarkus.security.identity.SecurityIdentityAugmentor}. + * {@link io.quarkus.security.PermissionChecker} methods always authorize matched {@link PermissionsAllowed#value()} + * permissions. This annotation attribute cannot grant access to permissions granted by the checker methods. */ String[] permissions() default {}; + /** + * Specify {@link SecurityIdentityAugmentor} CDI beans that should augment {@link SecurityIdentity} created with + * this annotation. By default, no identity augmentors are applied. Use this option if you need to test + * custom {@link PermissionsAllowed#permission()} added with the identity augmentors. + */ + Class[] augmentors() default {}; + /** * Adds attributes to a {@link SecurityIdentity} configured by this annotation. * The attributes can be retrieved by the {@link SecurityIdentity#getAttributes()} method.