Skip to content

Commit 3f0a441

Browse files
authored
Merge pull request #48529 from michalvavrik/feature/oidc-tenant-multiple-vals
Support linking multiple tenants to a tenant feature
2 parents de51d08 + 53ec88a commit 3f0a441

File tree

3 files changed

+234
-42
lines changed

3 files changed

+234
-42
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package io.quarkus.oidc.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.util.List;
7+
8+
import jakarta.enterprise.event.Observes;
9+
import jakarta.inject.Inject;
10+
import jakarta.inject.Singleton;
11+
import jakarta.json.JsonObject;
12+
import jakarta.ws.rs.GET;
13+
import jakarta.ws.rs.Path;
14+
15+
import org.hamcrest.Matchers;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
19+
import io.quarkus.arc.All;
20+
import io.quarkus.arc.Unremovable;
21+
import io.quarkus.oidc.Oidc;
22+
import io.quarkus.oidc.OidcTenantConfig;
23+
import io.quarkus.oidc.OidcTenantConfigBuilder;
24+
import io.quarkus.oidc.TenantFeature;
25+
import io.quarkus.oidc.TokenCustomizer;
26+
import io.quarkus.oidc.runtime.OidcConfig;
27+
import io.quarkus.security.Authenticated;
28+
import io.quarkus.test.QuarkusDevModeTest;
29+
import io.quarkus.test.common.QuarkusTestResource;
30+
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
31+
import io.restassured.RestAssured;
32+
import io.vertx.ext.web.RoutingContext;
33+
34+
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
35+
public class TokenCustomizersTest {
36+
37+
@RegisterExtension
38+
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
39+
.withApplicationRoot((jar) -> jar
40+
.addClasses(AccessTokenResource.class, GlobalTokenCustomizer.class, AccessTokenResource.Customizers.class,
41+
NamedOneAndTwoCustomizer.class, NamedOneAndTwoAndFourCustomizer.class));
42+
43+
@Test
44+
public void testTokenCustomizers() {
45+
RestAssured.given().auth().oauth2(getAccessToken()).get("/access-token")
46+
.then().statusCode(200).body(Matchers.is("default"));
47+
var customizers = RestAssured.get("/access-token/customizers").then().statusCode(200).extract()
48+
.as(AccessTokenResource.Customizers.class);
49+
assertTrue(customizers.global);
50+
assertFalse(customizers.namedOneAndTwoCustomizer);
51+
assertFalse(customizers.namedOneAndTwoAndFourCustomizer);
52+
RestAssured.given().auth().oauth2(getAccessToken()).get("/access-token/named-1")
53+
.then().statusCode(200).body(Matchers.is("named-1"));
54+
customizers = RestAssured.get("/access-token/customizers").then().statusCode(200).extract()
55+
.as(AccessTokenResource.Customizers.class);
56+
assertTrue(customizers.global);
57+
assertTrue(customizers.namedOneAndTwoCustomizer);
58+
assertTrue(customizers.namedOneAndTwoAndFourCustomizer);
59+
RestAssured.given().auth().oauth2(getAccessToken()).get("/access-token/named-2")
60+
.then().statusCode(200).body(Matchers.is("named-2"));
61+
customizers = RestAssured.get("/access-token/customizers").then().statusCode(200).extract()
62+
.as(AccessTokenResource.Customizers.class);
63+
assertTrue(customizers.global);
64+
assertTrue(customizers.namedOneAndTwoCustomizer);
65+
assertTrue(customizers.namedOneAndTwoAndFourCustomizer);
66+
RestAssured.given().auth().oauth2(getAccessToken()).get("/access-token/named-3")
67+
.then().statusCode(200).body(Matchers.is("named-3"));
68+
customizers = RestAssured.get("/access-token/customizers").then().statusCode(200).extract()
69+
.as(AccessTokenResource.Customizers.class);
70+
assertTrue(customizers.global);
71+
assertFalse(customizers.namedOneAndTwoCustomizer);
72+
assertFalse(customizers.namedOneAndTwoAndFourCustomizer);
73+
RestAssured.given().auth().oauth2(getAccessToken()).get("/access-token/named-4")
74+
.then().statusCode(200).body(Matchers.is("named-4"));
75+
customizers = RestAssured.get("/access-token/customizers").then().statusCode(200).extract()
76+
.as(AccessTokenResource.Customizers.class);
77+
assertTrue(customizers.global);
78+
assertFalse(customizers.namedOneAndTwoCustomizer);
79+
assertTrue(customizers.namedOneAndTwoAndFourCustomizer);
80+
}
81+
82+
private static String getAccessToken() {
83+
return KeycloakTestResourceLifecycleManager.getAccessToken("alice");
84+
}
85+
86+
@Unremovable
87+
@Singleton
88+
public static class GlobalTokenCustomizer implements TokenCustomizer {
89+
90+
volatile boolean called = false;
91+
92+
@Override
93+
public JsonObject customizeHeaders(JsonObject headers) {
94+
called = true;
95+
return null;
96+
}
97+
}
98+
99+
@Unremovable
100+
@Singleton
101+
@TenantFeature({ "named-1", "named-2" })
102+
public static class NamedOneAndTwoCustomizer implements TokenCustomizer {
103+
104+
volatile boolean called = false;
105+
106+
@Override
107+
public JsonObject customizeHeaders(JsonObject headers) {
108+
called = true;
109+
return null;
110+
}
111+
}
112+
113+
@Unremovable
114+
@Singleton
115+
@TenantFeature({ "named-1", "named-2", "named-4" })
116+
public static class NamedOneAndTwoAndFourCustomizer implements TokenCustomizer {
117+
118+
volatile boolean called = false;
119+
120+
@Override
121+
public JsonObject customizeHeaders(JsonObject headers) {
122+
called = true;
123+
return null;
124+
}
125+
}
126+
127+
@Path("/access-token")
128+
public static class AccessTokenResource {
129+
130+
private GlobalTokenCustomizer globalTokenCustomizer;
131+
private NamedOneAndTwoCustomizer namedOneAndTwoCustomizer;
132+
private NamedOneAndTwoAndFourCustomizer namedOneAndTwoAndFourCustomizer;
133+
134+
@Inject
135+
RoutingContext context;
136+
137+
public AccessTokenResource(@All List<TokenCustomizer> tokenCustomizers) {
138+
for (TokenCustomizer tokenCustomizer : tokenCustomizers) {
139+
if (tokenCustomizer instanceof GlobalTokenCustomizer i) {
140+
globalTokenCustomizer = i;
141+
} else if (tokenCustomizer instanceof NamedOneAndTwoCustomizer i) {
142+
namedOneAndTwoCustomizer = i;
143+
} else if (tokenCustomizer instanceof NamedOneAndTwoAndFourCustomizer i) {
144+
namedOneAndTwoAndFourCustomizer = i;
145+
}
146+
}
147+
}
148+
149+
@Authenticated
150+
@GET
151+
public String defaultTenantAccessTokenName() {
152+
return "default";
153+
}
154+
155+
@Authenticated
156+
@Path("/{tenant-id}")
157+
@GET
158+
public String namedTenantAccessTokenName() {
159+
return context.<OidcTenantConfig> get(OidcTenantConfig.class.getName()).tenantId().get();
160+
}
161+
162+
record Customizers(boolean global, boolean namedOneAndTwoCustomizer, boolean namedOneAndTwoAndFourCustomizer) {
163+
}
164+
165+
@Path("/customizers")
166+
@GET
167+
public Customizers customizers() {
168+
try {
169+
return new Customizers(globalTokenCustomizer.called, namedOneAndTwoCustomizer.called,
170+
namedOneAndTwoAndFourCustomizer.called);
171+
} finally {
172+
globalTokenCustomizer.called = false;
173+
namedOneAndTwoCustomizer.called = false;
174+
namedOneAndTwoAndFourCustomizer.called = false;
175+
}
176+
}
177+
178+
void configureOidc(@Observes Oidc oidc, OidcConfig oidcConfig) {
179+
var defaultTenantConfigBuilder = new OidcTenantConfigBuilder(OidcConfig.getDefaultTenant(oidcConfig));
180+
oidc.create(defaultTenantConfigBuilder.tenantId("named-1").build());
181+
oidc.create(defaultTenantConfigBuilder.tenantId("named-2").build());
182+
oidc.create(defaultTenantConfigBuilder.tenantId("named-3").build());
183+
oidc.create(defaultTenantConfigBuilder.tenantId("named-4").build());
184+
}
185+
}
186+
187+
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantFeature.java

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,19 @@
1010
import java.lang.annotation.Retention;
1111
import java.lang.annotation.Target;
1212

13-
import jakarta.enterprise.util.AnnotationLiteral;
1413
import jakarta.inject.Qualifier;
1514

1615
/**
17-
* Qualifier used to specify which named tenant is associated with one or more OIDC feature.
16+
* Qualifier used to specify which named tenants are associated with one or more OIDC feature.
1817
*/
1918
@Target({ METHOD, FIELD, PARAMETER, TYPE })
2019
@Retention(RUNTIME)
2120
@Documented
2221
@Qualifier
2322
public @interface TenantFeature {
2423
/**
25-
* Identifies an OIDC tenant to which a given feature applies.
24+
* Identifies OIDC tenants to which a given feature applies.
2625
*/
27-
String value();
26+
String[] value();
2827

29-
/**
30-
* Supports inline instantiation of the {@link TenantFeature} qualifier.
31-
*/
32-
final class TenantFeatureLiteral extends AnnotationLiteral<TenantFeature> implements TenantFeature {
33-
34-
private final String value;
35-
36-
private TenantFeatureLiteral(String value) {
37-
this.value = value;
38-
}
39-
40-
@Override
41-
public String value() {
42-
return value;
43-
}
44-
45-
@Override
46-
public String toString() {
47-
return "TenantFeatureLiteral [value=" + value + "]";
48-
}
49-
50-
public static TenantFeature of(String value) {
51-
return new TenantFeatureLiteral(value);
52-
}
53-
}
5428
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package io.quarkus.oidc.runtime;
22

3+
import java.lang.annotation.Annotation;
34
import java.util.ArrayList;
45
import java.util.List;
56

6-
import jakarta.enterprise.inject.Default;
7+
import jakarta.inject.Named;
8+
import jakarta.json.JsonObject;
79

810
import io.quarkus.arc.Arc;
911
import io.quarkus.arc.ArcContainer;
1012
import io.quarkus.arc.InstanceHandle;
1113
import io.quarkus.oidc.OIDCException;
1214
import io.quarkus.oidc.OidcTenantConfig;
1315
import io.quarkus.oidc.TenantFeature;
14-
import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral;
1516
import io.quarkus.oidc.TokenCustomizer;
1617

1718
public class TenantFeatureFinder {
@@ -35,25 +36,55 @@ public static TokenCustomizer find(OidcTenantConfig oidcConfig) {
3536
throw new OIDCException("Unable to find TokenCustomizer " + customizerName);
3637
}
3738
} else if (oidcConfig.tenantId().isPresent()) {
38-
return container
39-
.instance(TokenCustomizer.class, TenantFeature.TenantFeatureLiteral.of(oidcConfig.tenantId().get()))
40-
.get();
39+
List<TokenCustomizer> tokenCustomizers = find(oidcConfig, TokenCustomizer.class);
40+
if (tokenCustomizers.isEmpty()) {
41+
return null;
42+
}
43+
if (tokenCustomizers.size() == 1) {
44+
return tokenCustomizers.get(0);
45+
}
46+
return new TokenCustomizer() {
47+
@Override
48+
public JsonObject customizeHeaders(JsonObject headers) {
49+
JsonObject result = headers;
50+
for (TokenCustomizer tokenCustomizer : tokenCustomizers) {
51+
var customizedHeaders = tokenCustomizer.customizeHeaders(result);
52+
if (customizedHeaders != null) {
53+
result = customizedHeaders;
54+
}
55+
}
56+
return result == headers ? null : result;
57+
}
58+
};
4159
}
4260
}
4361
return null;
4462
}
4563

4664
public static <T> List<T> find(OidcTenantConfig oidcTenantConfig, Class<T> tenantFeatureClass) {
47-
if (oidcTenantConfig != null && oidcTenantConfig.tenantId().isPresent()) {
65+
ArcContainer container = Arc.container();
66+
if (oidcTenantConfig != null && container != null) {
4867
var tenantsValidators = new ArrayList<T>();
49-
for (var instance : Arc.container().listAll(tenantFeatureClass, Default.Literal.INSTANCE)) {
50-
if (instance.isAvailable()) {
51-
tenantsValidators.add(instance.get());
52-
}
53-
}
54-
for (var instance : Arc.container().listAll(tenantFeatureClass,
55-
TenantFeatureLiteral.of(oidcTenantConfig.tenantId().get()))) {
68+
allFeatureClasses: for (InstanceHandle<T> instance : container.listAll(tenantFeatureClass)) {
5669
if (instance.isAvailable()) {
70+
qualifiers: for (Annotation qualifier : instance.getBean().getQualifiers()) {
71+
if (qualifier instanceof TenantFeature tenantFeature) {
72+
String thisTenantId = oidcTenantConfig.tenantId().get();
73+
for (String thatTenantId : tenantFeature.value()) {
74+
if (thisTenantId.equals(thatTenantId)) {
75+
// adds tenant validator
76+
break qualifiers;
77+
}
78+
}
79+
// don't continue as this is a TenantFeature but not for our tenant
80+
continue allFeatureClasses;
81+
} else if (qualifier instanceof Named) {
82+
// following is done so that we don't include some features that are not meant to be global
83+
// but users want to include them using configuration properties
84+
// like 'io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer'
85+
continue allFeatureClasses;
86+
}
87+
}
5788
tenantsValidators.add(instance.get());
5889
}
5990
}

0 commit comments

Comments
 (0)