Skip to content

Commit

Permalink
Make Spring Security play with @testsecurity
Browse files Browse the repository at this point in the history
Follows up on: quarkusio#10487
Resolves: quarkusio#10490
  • Loading branch information
geoand committed Jul 7, 2020
1 parent 9f19c94 commit 99d8dab
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 84 deletions.
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions docs/src/main/asciidoc/security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -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();
}
}
}
8 changes: 7 additions & 1 deletion integration-tests/spring-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -56,8 +60,10 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.it.spring.web;

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest
public class SecurityIT extends SecurityTest {
}
Original file line number Diff line number Diff line change
@@ -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<String> content) {
assertStatusAndContent(RestAssured.given(), path, status, content);
}

private void assertForUsers(String path, int status, Optional<String> 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<String> content) {
ValidatableResponse validatableResponse = request.when().get(path)
.then()
.statusCode(status);
content.ifPresent(text -> validatableResponse.body(Matchers.equalTo(text)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> content) {
assertStatusAndContent(RestAssured.given(), path, status, content);
}

private void assertForUsers(String path, int status, Optional<String> 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<String> 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}
}

0 comments on commit 99d8dab

Please sign in to comment.