diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 452734184b14a..5836fb654cf7a 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -473,6 +473,11 @@ quarkus-security ${project.version} + + io.quarkus + quarkus-security-runtime-spi + ${project.version} + io.quarkus quarkus-security-deployment @@ -1787,6 +1792,11 @@ quarkus-junit5 ${project.version} + + io.quarkus + quarkus-test-security + ${project.version} + io.quarkus quarkus-junit5-internal diff --git a/docs/src/main/asciidoc/security.adoc b/docs/src/main/asciidoc/security.adoc index 2b6b769c755bb..a014dcafa4230 100644 --- a/docs/src/main/asciidoc/security.adoc +++ b/docs/src/main/asciidoc/security.adoc @@ -411,3 +411,50 @@ This will allow you to propagate the identity throughout the reactive callbacks. are using an executor that is capable of propagating the identity (e.g. no `CompletableFuture.supplyAsync`), to make sure that quarkus can propagate it. For more information see the link:context-propagation[Context Propagation Guide]. + +== Testing Security + +Quarkus provides explicit support for testing with different users, and with the security subsystem disabled. To use +this you must include the `quarkus-test-security` artifact: + +[source,xml] +---- + + io.quarkus + quarkus-test-security + test + +---- + +This artifact provides the `io.quarkus.test.security.TestSecurity` annotation, that can be applied to test methods and +test classes to control the security context that the test is run with. This allows you to do two things, you can disable +authorization so tests can access secured endpoints without needing to be authenticated, and you can specify the identity +that you want the tests to run under. + +A test that runs with authorization disabled can just set the enabled property to false: + +[source,java] +---- +@Test +@TestSecurity(authorizationEnabled = false) +void someTestMethod() { +... +} +---- + +This will disable all access checks, which allows the test to access secured endpoints without needing to authenticate. + +You can also use this to configure the current user that the test will run as: + +[source,java] +---- +@Test +@TestSecurity(user = "testUser", roles = {"admin", "user"}) +void someTestMethod() { +... +} +---- + +This will run the test with an identity with the given username and roles. Note that these can be combined, so you can +disable authorisation and also provide an identity to run the test under, which can be userful if the endpoint expects an +identity to be present. \ No newline at end of file diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java index 70d305e404bc7..0220d06890b22 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java +++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java @@ -43,11 +43,15 @@ public Response toResponse(UnauthorizedException exception) { if (authenticator != null) { ChallengeData challengeData = authenticator.getChallenge(context) .await().indefinitely(); - Response.ResponseBuilder status = Response.status(challengeData.status); - if (challengeData.headerName != null) { - status.header(challengeData.headerName.toString(), challengeData.headerContent); + if (challengeData != null) { + Response.ResponseBuilder status = Response.status(challengeData.status); + if (challengeData.headerName != null) { + status.header(challengeData.headerName.toString(), challengeData.headerContent); + } + return status.build(); + } else { + return Response.status(401).build(); } - return status.build(); } } return Response.status(401).entity("Not authorized").build(); diff --git a/extensions/security/deployment/pom.xml b/extensions/security/deployment/pom.xml index 4539e6116bfee..9214343675a7a 100644 --- a/extensions/security/deployment/pom.xml +++ b/extensions/security/deployment/pom.xml @@ -18,6 +18,10 @@ io.quarkus quarkus-arc-deployment + + io.quarkus + quarkus-security-runtime-spi + io.quarkus quarkus-security 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 e2606144bd777..96d417eb6ce4b 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 @@ -63,6 +63,7 @@ import io.quarkus.security.runtime.interceptor.SecurityHandler; import io.quarkus.security.runtime.interceptor.check.SecurityCheck; import io.quarkus.security.spi.AdditionalSecuredClassesBuildIem; +import io.quarkus.security.spi.runtime.AuthorizationController; public class SecurityProcessor { @@ -83,6 +84,11 @@ void services(BuildProducer jcaProviders) { } } + @BuildStep + AdditionalBeanBuildItem authorizationController() { + return AdditionalBeanBuildItem.builder().addBeanClass(AuthorizationController.class).build(); + } + /** * Register the classes for reflection in the requested named providers * diff --git a/extensions/security/pom.xml b/extensions/security/pom.xml index 0bdc3350a88df..022f2c65b4252 100644 --- a/extensions/security/pom.xml +++ b/extensions/security/pom.xml @@ -18,5 +18,6 @@ runtime spi test-utils + runtime-spi diff --git a/extensions/security/runtime-spi/pom.xml b/extensions/security/runtime-spi/pom.xml new file mode 100644 index 0000000000000..2f71d8bdae548 --- /dev/null +++ b/extensions/security/runtime-spi/pom.xml @@ -0,0 +1,39 @@ + + + + quarkus-security-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-security-runtime-spi + Quarkus - Security - Runtime SPI + + + + io.quarkus + quarkus-core + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + \ No newline at end of file diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java new file mode 100644 index 0000000000000..97c441257d855 --- /dev/null +++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java @@ -0,0 +1,14 @@ +package io.quarkus.security.spi.runtime; + +import javax.inject.Singleton; + +/** + * controller that allows authorization to be disabled in tests. + */ +@Singleton +public class AuthorizationController { + + public boolean isAuthorizationEnabled() { + return true; + } +} diff --git a/extensions/security/runtime/pom.xml b/extensions/security/runtime/pom.xml index 186fc9fcfc1ab..ca9190ddff144 100644 --- a/extensions/security/runtime/pom.xml +++ b/extensions/security/runtime/pom.xml @@ -18,6 +18,10 @@ io.quarkus quarkus-arc + + io.quarkus + quarkus-security-runtime-spi + jakarta.interceptor jakarta.interceptor-api diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java index cb900f3efc093..59aa863b2bc73 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java @@ -7,6 +7,7 @@ import javax.interceptor.InvocationContext; import io.quarkus.security.Authenticated; +import io.quarkus.security.spi.runtime.AuthorizationController; /** * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com @@ -19,8 +20,15 @@ public class AuthenticatedInterceptor { @Inject SecurityHandler handler; + @Inject + AuthorizationController controller; + @AroundInvoke public Object intercept(InvocationContext ic) throws Exception { - return handler.handle(ic); + if (controller.isAuthorizationEnabled()) { + return handler.handle(ic); + } else { + return ic.proceed(); + } } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java index 3d86997dc63f1..a0931f204fa38 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java @@ -7,6 +7,8 @@ import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; +import io.quarkus.security.spi.runtime.AuthorizationController; + /** * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com */ @@ -18,8 +20,15 @@ public class RolesAllowedInterceptor { @Inject SecurityHandler handler; + @Inject + AuthorizationController controller; + @AroundInvoke public Object intercept(InvocationContext ic) throws Exception { - return handler.handle(ic); + if (controller.isAuthorizationEnabled()) { + return handler.handle(ic); + } else { + return ic.proceed(); + } } } diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml index f6db46911cb02..8c46a6ea1869e 100644 --- a/extensions/vertx-http/runtime/pom.xml +++ b/extensions/vertx-http/runtime/pom.xml @@ -18,6 +18,10 @@ io.quarkus quarkus-core + + io.quarkus + quarkus-security-runtime-spi + io.quarkus quarkus-development-mode-spi diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 300f5ee54f57c..2f91fd7df1863 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -13,6 +13,7 @@ import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.spi.runtime.AuthorizationController; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; import io.smallrye.mutiny.subscription.UniSubscriber; @@ -31,6 +32,9 @@ public class HttpAuthorizer { @Inject IdentityProviderManager identityProviderManager; + @Inject + AuthorizationController controller; + final List policies; @Inject @@ -87,6 +91,10 @@ public void run() { * */ public void checkPermission(RoutingContext routingContext) { + if (!controller.isAuthorizationEnabled()) { + routingContext.next(); + return; + } //check their permissions doPermissionCheck(routingContext, QuarkusHttpUser.getSecurityIdentity(routingContext, identityProviderManager), 0, null, policies); diff --git a/integration-tests/elytron-resteasy/pom.xml b/integration-tests/elytron-resteasy/pom.xml index c3800d0f4400c..b05480a300df5 100644 --- a/integration-tests/elytron-resteasy/pom.xml +++ b/integration-tests/elytron-resteasy/pom.xml @@ -30,6 +30,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-test-security + test + io.rest-assured rest-assured 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 868445796d51b..a95deb79c4457 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 @@ -1,5 +1,6 @@ package io.quarkus.it.resteasy.elytron; +import javax.annotation.security.RolesAllowed; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -9,6 +10,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; +import io.quarkus.security.Authenticated; + @Path("/") public class RootResource { @@ -32,4 +35,19 @@ public String approval(@Context SecurityContext sec) { } return "get success"; } + + @GET + @Path("/secure") + @Authenticated + public String getSecure() { + return "secure"; + } + + @GET + @Path("/user") + @RolesAllowed("user") + public String user(@Context SecurityContext sec) { + return sec.getUserPrincipal().getName(); + } + } diff --git a/integration-tests/elytron-resteasy/src/main/resources/application.properties b/integration-tests/elytron-resteasy/src/main/resources/application.properties index 8a7b602db668f..d56f07473671d 100644 --- a/integration-tests/elytron-resteasy/src/main/resources/application.properties +++ b/integration-tests/elytron-resteasy/src/main/resources/application.properties @@ -5,5 +5,5 @@ quarkus.security.users.embedded.users.mary=mary quarkus.security.users.embedded.roles.mary=managers quarkus.security.users.embedded.users.poul=poul quarkus.security.users.embedded.roles.poul=interns -quarkus.security.users.embedded.auth-mechanism=BASIC quarkus.security.users.embedded.plain-text=true +quarkus.http.auth.basic=true \ No newline at end of file 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 new file mode 100644 index 0000000000000..bf1fca0974877 --- /dev/null +++ b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java @@ -0,0 +1,66 @@ +package io.quarkus.it.resteasy.elytron; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; + +@QuarkusTest +class TestSecurityTestCase { + + @Test + @TestSecurity(authorizationEnabled = false) + void testGet() { + given() + .when() + .get("/secure") + .then() + .statusCode(200) + .body(is("secure")); + } + + @Test + @TestSecurity + void testGetWithSecEnabled() { + given() + .when() + .get("/secure") + .then() + .statusCode(401); + } + + @Test + @TestSecurity(user = "testUser", roles = "wrong") + void testGetWithTestUser() { + given() + .when() + .get("/secure") + .then() + .statusCode(200); + } + + @Test + @TestSecurity(user = "testUser", roles = "wrong") + void testGetWithTestUserwrongRole() { + given() + .when() + .get("/user") + .then() + .statusCode(403); + } + + @Test + @TestSecurity(user = "testUser", roles = "user") + void testTestUserCorrectRole() { + given() + .when() + .get("/user") + .then() + .statusCode(200) + .body(is("testUser")); + } + +} diff --git a/test-framework/pom.xml b/test-framework/pom.xml index 4f10d79f93fe2..58e65f93aa880 100644 --- a/test-framework/pom.xml +++ b/test-framework/pom.xml @@ -28,6 +28,7 @@ maven vault ldap + security diff --git a/test-framework/security/pom.xml b/test-framework/security/pom.xml new file mode 100644 index 0000000000000..56d34268f01da --- /dev/null +++ b/test-framework/security/pom.xml @@ -0,0 +1,55 @@ + + + + io.quarkus + quarkus-test-framework + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-test-security + Quarkus - Test Framework - Security + Module that contains utilities to test Quarkus security + + + + io.quarkus + quarkus-junit5 + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-vertx-http + + + org.junit.jupiter + junit-jupiter + compile + + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + + + 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 new file mode 100644 index 0000000000000..ee0d1998ee96d --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -0,0 +1,61 @@ +package io.quarkus.test.security; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; + +import javax.enterprise.inject.spi.CDI; + +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; +import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; +import io.quarkus.test.junit.callback.QuarkusTestMethodContext; + +public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallback, QuarkusTestAfterEachCallback { + + @Override + public void afterEach(QuarkusTestMethodContext context) { + CDI.current().select(TestAuthController.class).get().setEnabled(true); + CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(null); + } + + @Override + public void beforeEach(QuarkusTestMethodContext context) { + try { + //the usual ClassLoader hacks to get our copy of the TestSecurity annotation + ClassLoader cl = QuarkusSecurityTestExtension.class.getClassLoader(); + Class original = cl.loadClass(context.getTestMethod().getDeclaringClass().getName()); + Class test = cl.loadClass(context.getTestInstance().getClass().getName()); + Method method = original.getDeclaredMethod(context.getTestMethod().getName(), + Arrays.stream(context.getTestMethod().getParameterTypes()).map(s -> { + try { + return cl.loadClass(s.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }).toArray(Class[]::new)); + TestSecurity testSecurity = method.getAnnotation(TestSecurity.class); + if (testSecurity == null) { + testSecurity = test.getAnnotation(TestSecurity.class); + } + if (testSecurity == null) { + return; + } + CDI.current().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"); + } + } else { + QuarkusSecurityIdentity user = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(testSecurity.user())) + .addRoles(new HashSet<>(Arrays.asList(testSecurity.roles()))).build(); + CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(user); + } + } catch (Exception e) { + throw new RuntimeException("Unable to setup @TestSecurity", e); + } + + } +} diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java new file mode 100644 index 0000000000000..f9960aec31103 --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java @@ -0,0 +1,40 @@ +package io.quarkus.test.security; + +import javax.annotation.PostConstruct; +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.interceptor.Interceptor; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.spi.runtime.AuthorizationController; + +@Alternative +@Priority(Interceptor.Priority.LIBRARY_AFTER) +@ApplicationScoped +public class TestAuthController extends AuthorizationController { + + @PostConstruct + public void check() { + if (LaunchMode.current() != LaunchMode.TEST) { + //paranoid check + throw new RuntimeException("TestAuthController can only be used in tests"); + } + } + + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public TestAuthController setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + @Override + public boolean isAuthorizationEnabled() { + return enabled; + } +} diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java new file mode 100644 index 0000000000000..2e77b2c4b7882 --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java @@ -0,0 +1,53 @@ +package io.quarkus.test.security; + +import java.util.Collections; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class TestHttpAuthenticationMechanism implements HttpAuthenticationMechanism { + + @Inject + TestIdentityAssociation testIdentityAssociation; + + @PostConstruct + public void check() { + if (LaunchMode.current() != LaunchMode.TEST) { + //paranoid check + throw new RuntimeException("TestAuthController can only be used in tests"); + } + } + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().nullItem(); + } + + @Override + public Set> getCredentialTypes() { + return Collections.emptySet(); + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return null; + } +} 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 new file mode 100644 index 0000000000000..3556adf608879 --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java @@ -0,0 +1,77 @@ +package io.quarkus.test.security; + +import javax.annotation.PostConstruct; +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Alternative; +import javax.inject.Inject; +import javax.interceptor.Interceptor; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.SecurityIdentityAssociation; +import io.smallrye.mutiny.Uni; + +@Alternative +@Priority(Interceptor.Priority.LIBRARY_AFTER) +@ApplicationScoped +public class TestIdentityAssociation extends SecurityIdentityAssociation { + + @PostConstruct + public void check() { + if (LaunchMode.current() != LaunchMode.TEST) { + //paranoid check + throw new RuntimeException("TestAuthController can only be used in tests"); + } + } + + SecurityIdentity testIdentity; + + /** + * A request scoped delegate that allows the system to function as normal when + * the user has not been explicitly overridden + */ + @Inject + DelegateSecurityIdentityAssociation delegate; + + public SecurityIdentity getTestIdentity() { + return testIdentity; + } + + public TestIdentityAssociation setTestIdentity(SecurityIdentity testIdentity) { + this.testIdentity = testIdentity; + return this; + } + + @Override + public void setIdentity(SecurityIdentity identity) { + delegate.setIdentity(identity); + } + + @Override + public void setIdentity(Uni identity) { + delegate.setIdentity(identity); + } + + @Override + public Uni getDeferredIdentity() { + if (testIdentity != null) { + return Uni.createFrom().item(testIdentity); + } + return delegate.getDeferredIdentity(); + } + + @Override + public SecurityIdentity getIdentity() { + if (testIdentity != null) { + return testIdentity; + } + return delegate.getIdentity(); + } +} + +@RequestScoped +class DelegateSecurityIdentityAssociation extends SecurityIdentityAssociation { + +} 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 new file mode 100644 index 0000000000000..c2f4414d82102 --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java @@ -0,0 +1,27 @@ +package io.quarkus.test.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface TestSecurity { + + /** + * If this is false then all security constraints are disabled. + */ + boolean authorizationEnabled() default true; + + /** + * If this is non-zero then the test will be run with a SecurityIdentity with the specified username. + */ + String user() default ""; + + /** + * Used in combination with {@link #user()} to specify the users roles. + */ + String[] roles() default {}; + +} diff --git a/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback new file mode 100644 index 0000000000000..3dec78b0521ba --- /dev/null +++ b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback @@ -0,0 +1 @@ +io.quarkus.test.security.QuarkusSecurityTestExtension \ No newline at end of file diff --git a/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback new file mode 100644 index 0000000000000..3dec78b0521ba --- /dev/null +++ b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback @@ -0,0 +1 @@ +io.quarkus.test.security.QuarkusSecurityTestExtension \ No newline at end of file