diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc
index d493db13965a1..a397c379a2aa6 100644
--- a/docs/src/main/asciidoc/getting-started-testing.adoc
+++ b/docs/src/main/asciidoc/getting-started-testing.adoc
@@ -698,6 +698,10 @@ public class GreetingResourceTest {
If you are using the `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extensions, check out the link:hibernate-orm-panache#mocking[Hibernate ORM with Panache Mocking] and link:mongodb-panache#mocking[MongoDB with Panache Mocking] documentation for the easiest way to mock your data access.
+== Testing Security
+
+If you are using Quarkus Security, check out the link:security.adoc#testing-security[Testing Security] section for information on how to easily test security features of the application.
+
[#quarkus-test-resource]
== Starting services before the Quarkus application starts
diff --git a/docs/src/main/asciidoc/security.adoc b/docs/src/main/asciidoc/security.adoc
index a014dcafa4230..598a34c75f943 100644
--- a/docs/src/main/asciidoc/security.adoc
+++ b/docs/src/main/asciidoc/security.adoc
@@ -412,6 +412,7 @@ are using an executor that is capable of propagating the identity (e.g. no `Comp
to make sure that quarkus can propagate it. For more information see the
link:context-propagation[Context Propagation Guide].
+[#testing-security]
== Testing Security
Quarkus provides explicit support for testing with different users, and with the security subsystem disabled. To use
@@ -456,5 +457,16 @@ 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
+disable authorization while also providing an identity to run the test under, which can be useful if the endpoint expects an
+identity to be present.
+
+[WARNING]
+====
+The feature is only available for `@QuarkusTest` and will **not** work on a `@NativeImageTest`.
+====
+
+=== Mixing security tests
+
+If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth
+mechanism when none is defined), then Basic Auth needs to be enabled explicitly,
+for example by setting `quarkus.http.auth.basic=true` or `%test.quarkus.http.auth.basic=true`.
diff --git a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringPreauthorizeInterceptor.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringPreauthorizeInterceptor.java
index c907db9e6eb46..fe5d02942e9d4 100644
--- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringPreauthorizeInterceptor.java
+++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringPreauthorizeInterceptor.java
@@ -9,6 +9,7 @@
import org.springframework.security.access.prepost.PreAuthorize;
import io.quarkus.security.runtime.interceptor.SecurityHandler;
+import io.quarkus.security.spi.runtime.AuthorizationController;
@Interceptor
@PreAuthorize("")
@@ -18,8 +19,15 @@ public class SpringPreauthorizeInterceptor {
@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/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecuredInterceptor.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecuredInterceptor.java
index 18a1d1cabbcd0..86ef67a99818b 100644
--- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecuredInterceptor.java
+++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecuredInterceptor.java
@@ -9,6 +9,7 @@
import org.springframework.security.access.annotation.Secured;
import io.quarkus.security.runtime.interceptor.SecurityHandler;
+import io.quarkus.security.spi.runtime.AuthorizationController;
@Interceptor
@Secured("")
@@ -18,8 +19,15 @@ public class SpringSecuredInterceptor {
@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/integration-tests/spring-web/pom.xml b/integration-tests/spring-web/pom.xml
index 497426f3c869c..1924fa5af79d8 100644
--- a/integration-tests/spring-web/pom.xml
+++ b/integration-tests/spring-web/pom.xml
@@ -43,6 +43,10 @@
io.quarkus
quarkus-hibernate-validator
+
+ io.quarkus
+ quarkus-elytron-security-properties-file
+
io.quarkus
@@ -56,8 +60,10 @@
io.quarkus
- quarkus-elytron-security-properties-file
+ quarkus-test-security
+ test
+
diff --git a/integration-tests/spring-web/src/main/resources/application.properties b/integration-tests/spring-web/src/main/resources/application.properties
index 24fb66ecc39d4..a639a8ea0a28d 100644
--- a/integration-tests/spring-web/src/main/resources/application.properties
+++ b/integration-tests/spring-web/src/main/resources/application.properties
@@ -3,3 +3,7 @@ quarkus.security.users.file.users=test-users.properties
quarkus.security.users.file.roles=test-roles.properties
quarkus.security.users.file.plain-text=true
%test.quarkus.http.test-port=0
+
+# we add this because we also use @TestSecurity which means that basic auth is disabled by default because @TestSecurity
+# contributes TestHttpAuthenticationMechanism
+%test.quarkus.http.auth.basic=true
diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityIT.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityIT.java
new file mode 100644
index 0000000000000..c1dc3f5705c88
--- /dev/null
+++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.it.spring.web;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class SecurityIT extends SecurityTest {
+}
diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java
new file mode 100644
index 0000000000000..02452e84e90e9
--- /dev/null
+++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SecurityTest.java
@@ -0,0 +1,89 @@
+package io.quarkus.it.spring.web;
+
+import java.util.Optional;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.response.ValidatableResponse;
+import io.restassured.specification.RequestSpecification;
+
+@QuarkusTest
+public class SecurityTest {
+
+ @Test
+ public void shouldRestrictAccessToSpecificRole() {
+ String path = "/api/securedMethod";
+ assertForAnonymous(path, 401, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 403, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), path, 200,
+ Optional.of("accessibleForAdminOnly"));
+ }
+
+ @Test
+ public void testAllowedForAdminOrViewer() {
+ String path = "/api/allowedForUserOrViewer";
+ assertForAnonymous(path, 401, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("aurea", "auri"), path, 403, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 200,
+ Optional.of("allowedForUserOrViewer"));
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("george", "geo"), path, 200,
+ Optional.of("allowedForUserOrViewer"));
+ }
+
+ @Test
+ public void testWithAlwaysFalseChecker() {
+ String path = "/api/withAlwaysFalseChecker";
+ assertForAnonymous(path, 401, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("george", "geo"), path, 403, Optional.empty());
+ }
+
+ @Test
+ public void testPreAuthorizeOnController() {
+ String path = "/api/preAuthorizeOnController";
+ assertForAnonymous(path, 401, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 200,
+ Optional.of("preAuthorizeOnController"));
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("aurea", "auri"), path, 200,
+ Optional.of("preAuthorizeOnController"));
+ }
+
+ @Test
+ public void shouldAccessAllowed() {
+ assertForAnonymous("/api/accessibleForAllMethod", 200, Optional.of("accessibleForAll"));
+ assertForUsers("/api/accessibleForAllMethod", 200, Optional.of("accessibleForAll"));
+ }
+
+ @Test
+ public void shouldRestrictAccessOnClass() {
+ assertForAnonymous("/api/restrictedOnClass", 401, Optional.empty());
+ assertForUsers("/api/restrictedOnClass", 200, Optional.of("restrictedOnClass"));
+ }
+
+ @Test
+ public void shouldFailToAccessRestrictedOnClass() {
+ assertForAnonymous("/api/restrictedOnMethod", 401, Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), "/api/restrictedOnMethod", 403,
+ Optional.empty());
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), "/api/restrictedOnMethod", 200,
+ Optional.of("restrictedOnMethod"));
+ }
+
+ private void assertForAnonymous(String path, int status, Optional content) {
+ assertStatusAndContent(RestAssured.given(), path, status, content);
+ }
+
+ private void assertForUsers(String path, int status, Optional content) {
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, status, content);
+ assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), path, status, content);
+ }
+
+ private void assertStatusAndContent(RequestSpecification request, String path, int status, Optional content) {
+ ValidatableResponse validatableResponse = request.when().get(path)
+ .then()
+ .statusCode(status);
+ content.ifPresent(text -> validatableResponse.body(Matchers.equalTo(text)));
+ }
+}
diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java
index 0cf9ce1bf292d..973667bf3db86 100644
--- a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java
+++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/SpringControllerTest.java
@@ -2,93 +2,14 @@
import static org.hamcrest.Matchers.containsString;
-import java.util.Optional;
-
-import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
-import io.restassured.response.ValidatableResponse;
-import io.restassured.specification.RequestSpecification;
@QuarkusTest
public class SpringControllerTest {
- @Test
- public void shouldRestrictAccessToSpecificRole() {
- String path = "/api/securedMethod";
- assertForAnonymous(path, 401, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 403, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), path, 200,
- Optional.of("accessibleForAdminOnly"));
- }
-
- @Test
- public void testAllowedForAdminOrViewer() {
- String path = "/api/allowedForUserOrViewer";
- assertForAnonymous(path, 401, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("aurea", "auri"), path, 403, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 200,
- Optional.of("allowedForUserOrViewer"));
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("george", "geo"), path, 200,
- Optional.of("allowedForUserOrViewer"));
- }
-
- @Test
- public void testWithAlwaysFalseChecker() {
- String path = "/api/withAlwaysFalseChecker";
- assertForAnonymous(path, 401, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("george", "geo"), path, 403, Optional.empty());
- }
-
- @Test
- public void testPreAuthorizeOnController() {
- String path = "/api/preAuthorizeOnController";
- assertForAnonymous(path, 401, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 200,
- Optional.of("preAuthorizeOnController"));
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("aurea", "auri"), path, 200,
- Optional.of("preAuthorizeOnController"));
- }
-
- @Test
- public void shouldAccessAllowed() {
- assertForAnonymous("/api/accessibleForAllMethod", 200, Optional.of("accessibleForAll"));
- assertForUsers("/api/accessibleForAllMethod", 200, Optional.of("accessibleForAll"));
- }
-
- @Test
- public void shouldRestrictAccessOnClass() {
- assertForAnonymous("/api/restrictedOnClass", 401, Optional.empty());
- assertForUsers("/api/restrictedOnClass", 200, Optional.of("restrictedOnClass"));
- }
-
- @Test
- public void shouldFailToAccessRestrictedOnClass() {
- assertForAnonymous("/api/restrictedOnMethod", 401, Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), "/api/restrictedOnMethod", 403,
- Optional.empty());
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), "/api/restrictedOnMethod", 200,
- Optional.of("restrictedOnMethod"));
- }
-
- private void assertForAnonymous(String path, int status, Optional content) {
- assertStatusAndContent(RestAssured.given(), path, status, content);
- }
-
- private void assertForUsers(String path, int status, Optional content) {
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, status, content);
- assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), path, status, content);
- }
-
- private void assertStatusAndContent(RequestSpecification request, String path, int status, Optional content) {
- ValidatableResponse validatableResponse = request.when().get(path)
- .then()
- .statusCode(status);
- content.ifPresent(text -> validatableResponse.body(Matchers.equalTo(text)));
- }
-
@Test
public void testJsonResult() {
RestAssured.when().get("/greeting/json/hello").then()
diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/TestSecurityTest.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/TestSecurityTest.java
new file mode 100644
index 0000000000000..75d3cdb4f958c
--- /dev/null
+++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/TestSecurityTest.java
@@ -0,0 +1,40 @@
+package io.quarkus.it.spring.web;
+
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+@TestSecurity(authorizationEnabled = false)
+public class TestSecurityTest {
+
+ @Test
+ public void testSecuredWithDisabledAuth() {
+ RestAssured.when().get("/api/securedMethod").then()
+ .body(is("accessibleForAdminOnly"));
+ }
+
+ @Test
+ public void testPreAuthorizeWithDisabledAuth() {
+ RestAssured.when().get("/api/allowedForUserOrViewer").then()
+ .body(is("allowedForUserOrViewer"));
+ }
+
+ @Test
+ @TestSecurity(user = "dummy", roles = "viewer")
+ public void testWithTestSecurityAndWrongRole() {
+ RestAssured.when().get("/api/securedMethod").then()
+ .statusCode(403);
+ }
+
+ @Test
+ @TestSecurity(user = "dummy", roles = "admin")
+ public void testWithTestSecurityAndCorrectRole() {
+ RestAssured.when().get("/api/securedMethod").then()
+ .body(is("accessibleForAdminOnly"));
+ }
+}