Skip to content

Commit 17a8b2e

Browse files
michalvavrikgsmet
authored andcommitted
Make sure identity produced by @TestSecurity is augmented
(cherry picked from commit e6a895b)
1 parent 18bc840 commit 17a8b2e

File tree

15 files changed

+282
-19
lines changed

15 files changed

+282
-19
lines changed

docs/src/main/asciidoc/security-testing.adoc

+44
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,50 @@ public String getDetail() {
156156
}
157157
----
158158

159+
It is also possible to set custom permissions like in the example below:
160+
161+
[source,java]
162+
----
163+
@PermissionsAllowed("see", permission = CustomPermission.class)
164+
public String getDetail() {
165+
return "detail";
166+
}
167+
----
168+
169+
The `CustomPermission` needs to be granted to the `SecurityIdentity` created
170+
by the `@TestSecurity` annotation with a `SecurityIdentityAugmentor` CDI bean:
171+
172+
[source,java]
173+
----
174+
@ApplicationScoped
175+
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
176+
@Override
177+
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
178+
AuthenticationRequestContext authenticationRequestContext) {
179+
final SecurityIdentity augmentedIdentity;
180+
if (shouldGrantCustomPermission(securityIdentity) {
181+
augmentedIdentity = QuarkusSecurityIdentity.builder(securityIdentity)
182+
.addPermission(new CustomPermission("see")).build();
183+
} else {
184+
augmentedIdentity = securityIdentity;
185+
}
186+
return Uni.createFrom().item(augmentedIdentity);
187+
}
188+
}
189+
----
190+
191+
Quarkus will only augment the `SecurityIdentity` created with the `@TestSecurity` annotation if you set
192+
the `@TestSecurity#augmentors` annotation attribute to the `CustomSecurityIdentityAugmentor.class` like this:
193+
194+
[source,java]
195+
----
196+
@Test
197+
@TestSecurity(user = "testUser", permissions = "see:detail", augmentors = CustomSecurityIdentityAugmentor.class)
198+
void someTestMethod() {
199+
...
200+
}
201+
----
202+
159203
=== Mixing security tests
160204

161205
If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth

extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
import io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder;
108108
import io.quarkus.security.identity.SecurityIdentityAugmentor;
109109
import io.quarkus.security.runtime.IdentityProviderManagerCreator;
110+
import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor;
110111
import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder;
111112
import io.quarkus.security.runtime.SecurityBuildTimeConfig;
112113
import io.quarkus.security.runtime.SecurityCheckRecorder;
@@ -691,7 +692,8 @@ void configurePermissionCheckers(PermissionSecurityChecksBuilderBuildItem checke
691692
// - this processor relies on the bean archive index (cycle: idx -> additional bean -> idx)
692693
// - we have injection points (=> better validation from Arc) as checker beans are only requested from this augmentor
693694
var syntheticBeanConfigurator = SyntheticBeanBuildItem
694-
.configure(SecurityIdentityAugmentor.class)
695+
.configure(QuarkusPermissionSecurityIdentityAugmentor.class)
696+
.addType(SecurityIdentityAugmentor.class)
695697
// ATM we do get augmentors from CDI once, no need to keep the instance in the CDI container
696698
.scope(Dependent.class)
697699
.unremovable()

extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* Adds a permission checker that grants access to the {@link QuarkusPermission}
1616
* when {@link QuarkusPermission#isGranted(SecurityIdentity)} is true.
1717
*/
18-
final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
18+
public final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
1919

2020
/**
2121
* Permission checker only authorizes authenticated users and checkers shouldn't throw a security exception.

extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import io.quarkus.runtime.annotations.Recorder;
3030
import io.quarkus.security.ForbiddenException;
3131
import io.quarkus.security.StringPermission;
32-
import io.quarkus.security.identity.SecurityIdentityAugmentor;
3332
import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder;
3433
import io.quarkus.security.runtime.interceptor.SecurityConstrainer;
3534
import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck;
@@ -436,10 +435,11 @@ private static Object convertMethodParamToPermParam(int i, Object methodArg,
436435
}
437436
}
438437

439-
public Function<SyntheticCreationalContext<SecurityIdentityAugmentor>, SecurityIdentityAugmentor> createPermissionAugmentor() {
440-
return new Function<SyntheticCreationalContext<SecurityIdentityAugmentor>, SecurityIdentityAugmentor>() {
438+
public Function<SyntheticCreationalContext<QuarkusPermissionSecurityIdentityAugmentor>, QuarkusPermissionSecurityIdentityAugmentor> createPermissionAugmentor() {
439+
return new Function<SyntheticCreationalContext<QuarkusPermissionSecurityIdentityAugmentor>, QuarkusPermissionSecurityIdentityAugmentor>() {
441440
@Override
442-
public SecurityIdentityAugmentor apply(SyntheticCreationalContext<SecurityIdentityAugmentor> ctx) {
441+
public QuarkusPermissionSecurityIdentityAugmentor apply(
442+
SyntheticCreationalContext<QuarkusPermissionSecurityIdentityAugmentor> ctx) {
443443
return new QuarkusPermissionSecurityIdentityAugmentor(ctx.getInjectedReference(BlockingSecurityExecutor.class));
444444
}
445445
};

integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import jakarta.ws.rs.core.SecurityContext;
1717

1818
import io.quarkus.security.Authenticated;
19+
import io.quarkus.security.PermissionChecker;
20+
import io.quarkus.security.PermissionsAllowed;
1921
import io.quarkus.security.identity.SecurityIdentity;
2022

2123
@Path("/")
@@ -73,4 +75,19 @@ public String getAttributes() {
7375
.map(e -> e.getKey() + "=" + e.getValue())
7476
.collect(Collectors.joining(","));
7577
}
78+
79+
@GET
80+
@Path("/test-security-permission-checker")
81+
@PermissionsAllowed("see-principal")
82+
public String getPrincipal(@Context SecurityContext sec) {
83+
return sec.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName();
84+
}
85+
86+
@PermissionChecker("see-principal")
87+
boolean canSeePrincipal(SecurityContext sec) {
88+
if (sec.getUserPrincipal() == null || sec.getUserPrincipal().getName() == null) {
89+
return false;
90+
}
91+
return "meat loaf".equals(sec.getUserPrincipal().getName());
92+
}
7693
}

integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java

+22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import jakarta.inject.Inject;
1313

14+
import org.hamcrest.Matchers;
1415
import org.junit.jupiter.api.Assertions;
1516
import org.junit.jupiter.api.Test;
1617
import org.junit.jupiter.params.ParameterizedTest;
@@ -23,6 +24,7 @@
2324
import io.quarkus.test.security.AttributeType;
2425
import io.quarkus.test.security.SecurityAttribute;
2526
import io.quarkus.test.security.TestSecurity;
27+
import io.restassured.RestAssured;
2628

2729
@QuarkusTest
2830
class TestSecurityTestCase {
@@ -187,4 +189,24 @@ static Stream<Arguments> arrayParams() {
187189
arguments(new int[] { 1, 2 }, new String[] { "hello", "world" }));
188190
}
189191

192+
@Test
193+
public void testPermissionChecker_anonymousUser() {
194+
// user is not authenticated and access should not be granted by the permission checker
195+
RestAssured.get("/test-security-permission-checker").then().statusCode(401);
196+
}
197+
198+
@Test
199+
@TestSecurity(user = "authenticated-user")
200+
public void testPermissionChecker_authenticatedUser() {
201+
// user is authenticated, but access should not be granted by the permission checker
202+
RestAssured.get("/test-security-permission-checker").then().statusCode(403);
203+
}
204+
205+
@Test
206+
@TestSecurity(user = "meat loaf")
207+
public void testPermissionChecker_authorizedUser() {
208+
// user is authenticated and access should be granted by the permission checker
209+
RestAssured.get("/test-security-permission-checker").then().statusCode(200)
210+
.body(Matchers.is("meat loaf:meat loaf:meat loaf"));
211+
}
190212
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.quarkus.it.keycloak;
2+
3+
import java.security.BasicPermission;
4+
5+
public class CustomPermission extends BasicPermission {
6+
7+
public CustomPermission(String name) {
8+
super(name);
9+
}
10+
}

integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.eclipse.microprofile.jwt.JsonWebToken;
1515

1616
import io.quarkus.security.Authenticated;
17+
import io.quarkus.security.PermissionsAllowed;
1718
import io.quarkus.security.identity.SecurityIdentity;
1819

1920
@Path("/web-app")
@@ -40,6 +41,14 @@ public String testSecurity() {
4041
+ principal.getName();
4142
}
4243

44+
@GET
45+
@Path("test-security-with-augmentors")
46+
@PermissionsAllowed(permission = CustomPermission.class, value = "augmented")
47+
public String testSecurityWithAugmentors() {
48+
return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":"
49+
+ principal.getName();
50+
}
51+
4352
@POST
4453
@Path("test-security")
4554
@Consumes("application/json")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.quarkus.it.keycloak;
2+
3+
import jakarta.enterprise.context.ApplicationScoped;
4+
5+
import io.quarkus.security.identity.AuthenticationRequestContext;
6+
import io.quarkus.security.identity.SecurityIdentity;
7+
import io.quarkus.security.identity.SecurityIdentityAugmentor;
8+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
9+
import io.smallrye.mutiny.Uni;
10+
11+
@ApplicationScoped
12+
public class TestSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
13+
14+
private static volatile boolean invoked = false;
15+
16+
@Override
17+
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
18+
AuthenticationRequestContext authenticationRequestContext) {
19+
invoked = true;
20+
final SecurityIdentity identity;
21+
if (securityIdentity.isAnonymous() || !"authorized-user".equals(securityIdentity.getPrincipal().getName())) {
22+
identity = securityIdentity;
23+
} else {
24+
identity = QuarkusSecurityIdentity.builder(securityIdentity)
25+
.addPermission(new CustomPermission("augmented")).build();
26+
}
27+
return Uni.createFrom().item(identity);
28+
}
29+
30+
public static boolean isInvoked() {
31+
return invoked;
32+
}
33+
34+
public static void resetInvoked() {
35+
invoked = false;
36+
}
37+
}

integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
quarkus.keycloak.devservices.enabled=false
2+
13
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
24
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
35
smallrye.jwt.path.groups=realm_access/roles

integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java

+48
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.lang.annotation.RetentionPolicy;
88
import java.lang.annotation.Target;
99

10+
import org.junit.jupiter.api.Assertions;
1011
import org.junit.jupiter.api.Test;
1112

1213
import io.quarkus.test.common.http.TestHTTPEndpoint;
@@ -21,6 +22,53 @@
2122
@TestHTTPEndpoint(ProtectedJwtResource.class)
2223
public class TestSecurityLazyAuthTest {
2324

25+
@Test
26+
public void testTestSecurityAnnotationWithAugmentors_anonymousUser() {
27+
TestSecurityIdentityAugmentor.resetInvoked();
28+
// user is not authenticated and doesn't have required role granted by the augmentor
29+
RestAssured.get("test-security-with-augmentors").then().statusCode(401);
30+
// identity manager applies augmentors on anonymous identity
31+
// because @TestSecurity is not in action and that's what we do for the anonymous requests
32+
Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked());
33+
}
34+
35+
@TestSecurity(user = "authenticated-user")
36+
@Test
37+
public void testTestSecurityAnnotationNoAugmentors_authenticatedUser() {
38+
TestSecurityIdentityAugmentor.resetInvoked();
39+
// user is authenticated, but doesn't have required role granted by the augmentor
40+
// and no augmentors are applied
41+
RestAssured.get("test-security-with-augmentors").then().statusCode(403);
42+
Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked());
43+
}
44+
45+
@TestSecurity(user = "authenticated-user", augmentors = TestSecurityIdentityAugmentor.class)
46+
@Test
47+
public void testTestSecurityAnnotationWithAugmentors_authenticatedUser() {
48+
TestSecurityIdentityAugmentor.resetInvoked();
49+
// user is authenticated, but doesn't have required role granted by the augmentor
50+
RestAssured.get("test-security-with-augmentors").then().statusCode(403);
51+
Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked());
52+
}
53+
54+
@TestSecurity(user = "authorized-user")
55+
@Test
56+
public void testTestSecurityAnnotationNoAugmentors_authorizedUser() {
57+
// should fail because no augmentors are applied
58+
TestSecurityIdentityAugmentor.resetInvoked();
59+
RestAssured.get("test-security-with-augmentors").then().statusCode(403);
60+
Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked());
61+
}
62+
63+
@TestSecurity(user = "authorized-user", augmentors = TestSecurityIdentityAugmentor.class)
64+
@Test
65+
public void testTestSecurityAnnotationWithAugmentors_authorizedUser() {
66+
TestSecurityIdentityAugmentor.resetInvoked();
67+
RestAssured.get("test-security-with-augmentors").then().statusCode(200)
68+
.body(is("authorized-user:authorized-user:authorized-user"));
69+
Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked());
70+
}
71+
2472
@Test
2573
@TestAsUser1Viewer
2674
public void testWithDummyUser() {

test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
package io.quarkus.test.security;
22

3+
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.ROUTING_CONTEXT_ATTRIBUTE;
4+
35
import java.util.Collections;
6+
import java.util.List;
7+
import java.util.Map;
48
import java.util.Set;
9+
import java.util.function.Supplier;
510

611
import jakarta.annotation.PostConstruct;
12+
import jakarta.enterprise.inject.Instance;
713
import jakarta.inject.Inject;
814

915
import io.quarkus.runtime.LaunchMode;
16+
import io.quarkus.security.identity.AuthenticationRequestContext;
1017
import io.quarkus.security.identity.IdentityProviderManager;
1118
import io.quarkus.security.identity.SecurityIdentity;
19+
import io.quarkus.security.identity.SecurityIdentityAugmentor;
1220
import io.quarkus.security.identity.request.AuthenticationRequest;
21+
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
1322
import io.quarkus.vertx.http.runtime.security.ChallengeData;
1423
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
1524
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
@@ -21,7 +30,11 @@ abstract class AbstractTestHttpAuthenticationMechanism implements HttpAuthentica
2130
@Inject
2231
TestIdentityAssociation testIdentityAssociation;
2332

33+
@Inject
34+
BlockingSecurityExecutor blockingSecurityExecutor;
35+
2436
protected volatile String authMechanism = null;
37+
protected volatile List<Instance<? extends SecurityIdentityAugmentor>> augmentors = null;
2538

2639
@PostConstruct
2740
public void check() {
@@ -32,8 +45,21 @@ public void check() {
3245
}
3346

3447
@Override
35-
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
36-
return Uni.createFrom().item(testIdentityAssociation.getTestIdentity());
48+
public Uni<SecurityIdentity> authenticate(RoutingContext event, IdentityProviderManager identityProviderManager) {
49+
var identity = Uni.createFrom().item(testIdentityAssociation.getTestIdentity());
50+
if (augmentors != null && testIdentityAssociation.getTestIdentity() != null) {
51+
var requestContext = new AuthenticationRequestContext() {
52+
@Override
53+
public Uni<SecurityIdentity> runBlocking(Supplier<SecurityIdentity> supplier) {
54+
return blockingSecurityExecutor.executeBlocking(supplier);
55+
}
56+
};
57+
var requestAttributes = Map.<String, Object> of(ROUTING_CONTEXT_ATTRIBUTE, event);
58+
for (var augmentor : augmentors) {
59+
identity = identity.flatMap(i -> augmentor.get().augment(i, requestContext, requestAttributes));
60+
}
61+
}
62+
return identity;
3763
}
3864

3965
@Override
@@ -55,4 +81,8 @@ public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext contex
5581
void setAuthMechanism(String authMechanism) {
5682
this.authMechanism = authMechanism;
5783
}
84+
85+
void setSecurityIdentityAugmentors(List<Instance<? extends SecurityIdentityAugmentor>> augmentors) {
86+
this.augmentors = augmentors;
87+
}
5888
}

0 commit comments

Comments
 (0)