Skip to content

Commit

Permalink
Resolve OIDC tenant with path-matching configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Mar 6, 2024
1 parent 3f1c078 commit aad3f58
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 480 deletions.
11 changes: 11 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,17 @@ quarkus.http.auth.permission.authenticated.applies-to=JAXRS <1>
----
<1> Tell Quarkus to run the HTTP permission check after the tenant has been selected with the `@Tenant` annotation.

=== Resolve with configuration

You can use the `quarkus.oidc.paths` configuration property for resolving the tenant identifier as an alternative to using `io.quarkus.oidc.TenantResolver`.

Check warning on line 742 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 742, "column": 70}}}, "severity": "INFO"}

Check warning on line 742 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 742, "column": 97}}}, "severity": "INFO"}

Check warning on line 742 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 742, "column": 117}}}, "severity": "INFO"}
Here is how you can select the `hr` tenant for the `sayHello` endpoint of the `HelloResource` resource used in the previous example:

[source,properties]
----
quarkus.oidc.hr.paths=/api/hello <1>
----
<1> Same path-matching rules apply as for the `quarkus.http.auth.permission.authenticated.paths=/api/hello` configuration property from the previous example.

Check warning on line 749 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 749, "column": 36}}}, "severity": "INFO"}

[[tenant-config-resolver]]
== Dynamic tenant configuration resolution

Check warning on line 752 in docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc", "range": {"start": {"line": 752, "column": 18}}}, "severity": "INFO"}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ public class OidcTenantConfig extends OidcCommonConfig {
@ConfigItem
public Optional<String> endSessionPath = Optional.empty();

/**
* The paths this tenant applies to. Static tenants can be resolved based on path-matching patterns. Path-matching is
* done exactly in a same manner as is done for the 'quarkus.http.auth.permission.perm-1.paths' configuration property paths.
*/
@ConfigItem
public Optional<List<String>> paths = Optional.empty();

/**
* The public key for the local JWT token verification.
* OIDC server connection is not created when this property is set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

Expand All @@ -38,8 +39,11 @@ public class DefaultTenantConfigResolver {
private static final String CURRENT_STATIC_TENANT_ID = "static.tenant.id";
private static final String CURRENT_STATIC_TENANT_ID_NULL = "static.tenant.id.null";
private static final String CURRENT_DYNAMIC_TENANT_CONFIG = "dynamic.tenant.config";

private DefaultStaticTenantResolver defaultStaticTenantResolver = new DefaultStaticTenantResolver();
private final ConcurrentHashMap<String, BackChannelLogoutTokenCache> backChannelLogoutTokens = new ConcurrentHashMap<>();
private final BlockingTaskRunner<OidcTenantConfig> blockingRequestContext;
private final boolean securityEventObserved;
private final DefaultStaticTenantResolver defaultStaticTenantResolver;
private final TenantConfigBean tenantConfigBean;

@Inject
Instance<TenantResolver> tenantResolver;
Expand All @@ -50,9 +54,6 @@ public class DefaultTenantConfigResolver {
@Inject
Instance<JavaScriptRequestChecker> javaScriptRequestChecker;

@Inject
TenantConfigBean tenantConfigBean;

@Inject
Instance<TokenStateManager> tokenStateManager;

Expand All @@ -69,17 +70,15 @@ public class DefaultTenantConfigResolver {
@ConfigProperty(name = "quarkus.http.proxy.enable-forwarded-prefix")
boolean enableHttpForwardedPrefix;

private final BlockingTaskRunner<OidcTenantConfig> blockingRequestContext;

private final boolean securityEventObserved;

private ConcurrentHashMap<String, BackChannelLogoutTokenCache> backChannelLogoutTokens = new ConcurrentHashMap<>();

public DefaultTenantConfigResolver(BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
DefaultTenantConfigResolver(BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled,
@ConfigProperty(name = "quarkus.http.root-path") String rootPath, TenantConfigBean tenantConfigBean) {
this.blockingRequestContext = new BlockingTaskRunner<OidcTenantConfig>(blockingExecutor);
this.securityEventObserved = SecurityEventHelper.isEventObserved(new SecurityEvent(null, (SecurityIdentity) null),
beanManager, securityEventsEnabled);
this.tenantConfigBean = tenantConfigBean;
this.defaultStaticTenantResolver = new DefaultStaticTenantResolver(tenantConfigBean.getStaticTenantsConfig(), rootPath,
tenantConfigBean.getDefaultTenant());
}

@PostConstruct
Expand Down Expand Up @@ -271,13 +270,37 @@ public JavaScriptRequestChecker getJavaScriptRequestChecker() {
}

private class DefaultStaticTenantResolver implements TenantResolver {
private static final String DEFAULT_TENANT = "DefaultStaticTenantResolver#DefaultTenant";
private final ImmutablePathMatcher<String> staticTenantPaths;

private DefaultStaticTenantResolver(Map<String, TenantConfigContext> staticTenantsConfig, String rootPath,
TenantConfigContext defaultTenant) {
final var builder = ImmutablePathMatcher.<String> builder().rootPath(rootPath);
addPath(DEFAULT_TENANT, defaultTenant.oidcConfig, builder);
for (Map.Entry<String, TenantConfigContext> e : staticTenantsConfig.entrySet()) {
addPath(e.getKey(), e.getValue().oidcConfig, builder);
}
this.staticTenantPaths = builder.hasPaths() ? builder.build() : null;
}

@Override
public String resolve(RoutingContext context) {
String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
if (tenantId != null) {
return tenantId;
}

if (staticTenantPaths != null) {
tenantId = staticTenantPaths.match(context.normalizedPath()).getValue();
if (tenantId != null) {
if (tenantId == DEFAULT_TENANT) {
return null;
} else {
return tenantId;
}
}
}

String[] pathSegments = context.request().path().split("/");
if (pathSegments.length > 0) {
String lastPathSegment = pathSegments[pathSegments.length - 1];
Expand All @@ -288,6 +311,15 @@ public String resolve(RoutingContext context) {
return null;
}

private static ImmutablePathMatcher.ImmutablePathMatcherBuilder<String> addPath(String tenant, OidcTenantConfig config,
ImmutablePathMatcher.ImmutablePathMatcherBuilder<String> builder) {
if (config != null && config.paths.isPresent()) {
for (String path : config.paths.get()) {
builder.addPath(path, tenant);
}
}
return builder;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public AbstractPathMatchingHttpSecurityPolicy(Map<String, PolicyMappingConfig> p
boolean hasNoPermissions = permissions.isEmpty();
var namedHttpSecurityPolicies = toNamedHttpSecPolicies(rolePolicy, installedPolicies);
List<ImmutablePathMatcher<List<HttpMatcher>>> sharedPermsMatchers = new ArrayList<>();
final var builder = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll);
final var builder = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll)
.rootPath(rootPath);
for (PolicyMappingConfig policyMappingConfig : permissions.values()) {
if (appliesTo != policyMappingConfig.appliesTo) {
continue;
Expand All @@ -55,11 +56,12 @@ public AbstractPathMatchingHttpSecurityPolicy(Map<String, PolicyMappingConfig> p
hasNoPermissions = false;
}
if (policyMappingConfig.shared) {
final var builder1 = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll);
addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, policyMappingConfig, builder1);
final var builder1 = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll)
.rootPath(rootPath);
addPermissionToPathMatcher(namedHttpSecurityPolicies, policyMappingConfig, builder1);
sharedPermsMatchers.add(builder1.build());
} else {
addPermissionToPathMatcher(namedHttpSecurityPolicies, rootPath, policyMappingConfig, builder);
addPermissionToPathMatcher(namedHttpSecurityPolicies, policyMappingConfig, builder);
}
}
this.hasNoPermissions = hasNoPermissions;
Expand Down Expand Up @@ -149,7 +151,7 @@ private static String getAuthMechanismName(RoutingContext routingContext,
return null;
}

private static void addPermissionToPathMatcher(Map<String, HttpSecurityPolicy> permissionCheckers, String rootPath,
private static void addPermissionToPathMatcher(Map<String, HttpSecurityPolicy> permissionCheckers,
PolicyMappingConfig policyMappingConfig,
ImmutablePathMatcher.ImmutablePathMatcherBuilder<List<HttpMatcher>> builder) {
HttpSecurityPolicy checker = permissionCheckers.get(policyMappingConfig.policy);
Expand All @@ -159,10 +161,6 @@ private static void addPermissionToPathMatcher(Map<String, HttpSecurityPolicy> p

if (policyMappingConfig.enabled.orElse(Boolean.TRUE)) {
for (String path : policyMappingConfig.paths.orElse(Collections.emptyList())) {
path = path.trim();
if (!path.startsWith("/")) {
path = rootPath + path;
}
HttpMatcher m = new HttpMatcher(policyMappingConfig.authMechanism.orElse(null),
new HashSet<>(policyMappingConfig.methods.orElse(Collections.emptyList())), checker);
List<HttpMatcher> perms = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public static class ImmutablePathMatcherBuilder<T> {
private final Map<String, T> additionalExactPathMatches = new HashMap<>();
private final Map<String, Path<T>> pathsWithWildcard = new HashMap<>();
private BiConsumer<T, T> handlerAccumulator;
private String rootPath;
private boolean empty = true;

private ImmutablePathMatcherBuilder() {
}
Expand All @@ -146,9 +148,22 @@ public ImmutablePathMatcherBuilder<T> handlerAccumulator(BiConsumer<T, T> handle
return this;
}

public boolean hasPaths() {
return !empty;
}

/**
* @param rootPath Path to which relative patterns (paths not starting with a separator) are linked.
* @return ImmutablePathMatcherBuilder
*/
public ImmutablePathMatcherBuilder<T> rootPath(String rootPath) {
this.rootPath = rootPath;
return this;
}

public ImmutablePathMatcher<T> build() {
T defaultHandler = null;
SubstringMap<T> paths = new SubstringMap<>();
var paths = ImmutableSubstringMap.<T> builder();
boolean hasPathWithInnerWildcard = false;
// process paths with a wildcard first, that way we only create inner path matcher when really needed
for (Path<T> p : pathsWithWildcard.values()) {
Expand Down Expand Up @@ -200,7 +215,7 @@ public void accept(SubstringMatch<T> match1, SubstringMatch<T> match2) {
exactPathMatches.putIfAbsent(e.getKey(), e.getValue());
}
int[] lengths = buildLengths(paths.keys());
return new ImmutablePathMatcher<>(defaultHandler, paths.asImmutableMap(), exactPathMatches, lengths,
return new ImmutablePathMatcher<>(defaultHandler, paths.build(), exactPathMatches, lengths,
hasPathWithInnerWildcard);
}

Expand All @@ -227,6 +242,13 @@ public void accept(SubstringMatch<T> match1, SubstringMatch<T> match2) {
* @return self
*/
public ImmutablePathMatcherBuilder<T> addPath(String path, T handler) {
if (empty) {
empty = false;
}
path = path.trim();
if (rootPath != null && !path.startsWith("/")) {
path = rootPath + path;
}
return addPath(path, path, handler);
}

Expand Down Expand Up @@ -363,13 +385,6 @@ public void addPrefixPath(T prefixPathHandler, BiConsumer<T, T> handlerAccumulat
}
}

private static class PathWithInnerWildcard<T> {
private final String remaining;
private final T handler;

private PathWithInnerWildcard(String remaining, T handler) {
this.remaining = remaining;
this.handler = handler;
}
private record PathWithInnerWildcard<T>(String remaining, T handler) {
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.vertx.http.runtime.security;

import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;

import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher.PathMatch;

Expand Down Expand Up @@ -132,4 +134,101 @@ boolean hasSubPathMatcher() {
return hasSubPathMatcher;
}
}

static <V> SubstringMapBuilder<V> builder() {
return new SubstringMapBuilder<>();
}

static final class SubstringMapBuilder<V> {
private Object[] table = new Object[16];
private int size;

private SubstringMapBuilder() {
}

void put(String key, V value, ImmutablePathMatcher<SubstringMatch<V>> subPathMatcher) {
if (key == null) {
throw new NullPointerException();
}

Object[] newTable;
if (table.length / (double) size < 4 && table.length != Integer.MAX_VALUE) {
newTable = new Object[table.length << 1];
for (int i = 0; i < table.length; i += 2) {
if (table[i] != null) {
doPut(newTable, (String) table[i], table[i + 1]);
}
}
} else {
newTable = new Object[table.length];
System.arraycopy(table, 0, newTable, 0, table.length);
}
doPut(newTable, key, new SubstringMatch<>(key, value, subPathMatcher));
this.table = newTable;
size++;
}

private void doPut(Object[] newTable, String key, Object value) {
int hash = hash(key, key.length());
int pos = tablePos(newTable, hash);
while (newTable[pos] != null && !newTable[pos].equals(key)) {
pos += 2;
if (pos >= newTable.length) {
pos = 0;
}
}
newTable[pos] = key;
newTable[pos + 1] = value;
}

public Iterable<String> keys() {
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
final Object[] tMap = table;
int i = 0;
while (i < table.length && tMap[i] == null) {
i += 2;
}
final int startPos = i;

return new Iterator<String>() {

private Object[] map = tMap;

private int pos = startPos;

@Override
public boolean hasNext() {
return pos < table.length;
}

@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String ret = (String) map[pos];

pos += 2;
while (pos < table.length && tMap[pos] == null) {
pos += 2;
}
return ret;
}

@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};

}

ImmutableSubstringMap<V> build() {
return new ImmutableSubstringMap<>(table);
}
}
}
Loading

0 comments on commit aad3f58

Please sign in to comment.