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")); + } +}