Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Keycloak Authorization dynamic tenant config resolution #39643

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/src/main/asciidoc/security-keycloak-authorization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,65 @@
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim
----

== Dynamic tenant configuration resolution

Check warning on line 562 in docs/src/main/asciidoc/security-keycloak-authorization.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-keycloak-authorization.adoc", "range": {"start": {"line": 562, "column": 29}}}, "severity": "INFO"}

If you need a more dynamic configuration for the different tenants you want to support and don’t want to end up
with multiple entries in your configuration file, you can use the `io.quarkus.keycloak.pep.TenantPolicyConfigResolver`.

This interface allows you to dynamically create tenant configurations at runtime:

[source,java]
----
package org.acme.security.keycloak.authorization;

import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.keycloak.pep.TenantPolicyConfigResolver;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantPolicyConfigResolver implements TenantPolicyConfigResolver {

private final KeycloakPolicyEnforcerTenantConfig enhancedTenantConfig;
private final KeycloakPolicyEnforcerTenantConfig newTenantConfig;

public CustomTenantPolicyConfigResolver(KeycloakPolicyEnforcerConfig enforcerConfig) {
this.enhancedTenantConfig = KeycloakPolicyEnforcerTenantConfig.builder(config) <1>
.paths("/enhanced-config")
.permissionName("Permission Name")
.get("read-scope")
.build();
this.newTenantConfig = KeycloakPolicyEnforcerTenantConfig.builder() <2>
.paths("/new-config")
.claimInformationPoint(Map.of("claims", Map.of("grant", "{request.parameter['grant']}")))
.build();
}

@Override
public Uni<KeycloakPolicyEnforcerTenantConfig> resolve(RoutingContext routingContext, OidcTenantConfig tenantConfig,
OidcRequestContext<KeycloakPolicyEnforcerTenantConfig> requestContext) {
String path = routingContext.normalizedPath();
String tenantId = tenantConfig.tenantId.orElse(null);
if ("enhanced-config-tenant".equals(tenantId) && path.equals("/enhanced-config")) {
return Uni.createFrom().item(enhancedTenantConfig);
} else if ("new-config-tenant".equals(tenantId) && path.equals("/new-config")) {
return Uni.createFrom().item(newTenantConfig);
}
return Uni.createFrom().nullItem(); <3>
}
}
----
<1> Create or update the `/enhanced-config` path in the default tenant config.
<2> Add `/new-config` path into tenant config populated with documented configuration default values.
<3> Use default static tenant configuration resolution based on the `application.properties` file and other SmallRye Config configuration sources.

== Configuration reference

This configuration adheres to the official [Keycloak Policy Enforcer Configuration](https://www.keycloak.org/docs/latest/authorization_services/index.html#_enforcer_filter) guidelines.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@

import java.util.function.BooleanSupplier;

import jakarta.inject.Singleton;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.keycloak.pep.runtime.DefaultPolicyEnforcerResolver;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerAuthorizer;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerBuildTimeConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerRecorder;
import io.quarkus.keycloak.pep.runtime.PolicyEnforcerResolver;
import io.quarkus.oidc.deployment.OidcBuildTimeConfig;
import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.tls.TlsRegistryBuildItem;
import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem;
import io.quarkus.vertx.http.runtime.HttpConfiguration;

@BuildSteps(onlyIf = KeycloakPolicyEnforcerBuildStep.IsEnabled.class)
public class KeycloakPolicyEnforcerBuildStep {
Expand All @@ -41,7 +35,9 @@ RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig,
public AdditionalBeanBuildItem beans(OidcBuildTimeConfig oidcBuildTimeConfig) {
if (oidcBuildTimeConfig.enabled) {
return AdditionalBeanBuildItem.builder().setUnremovable()
.addBeanClass(KeycloakPolicyEnforcerAuthorizer.class).build();
.addBeanClass(KeycloakPolicyEnforcerAuthorizer.class)
.addBeanClass(DefaultPolicyEnforcerResolver.class)
.build();
}
return null;
}
Expand All @@ -51,23 +47,6 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() {
return new ExtensionSslNativeSupportBuildItem(Feature.KEYCLOAK_AUTHORIZATION);
}

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
public SyntheticBeanBuildItem setup(OidcBuildTimeConfig oidcBuildTimeConfig, OidcConfig oidcRunTimeConfig,
KeycloakPolicyEnforcerConfig keycloakConfig, KeycloakPolicyEnforcerRecorder recorder,
HttpConfiguration httpConfiguration, TlsRegistryBuildItem tlsRegistryBuildItem) {
if (oidcBuildTimeConfig.enabled) {
return SyntheticBeanBuildItem.configure(PolicyEnforcerResolver.class).unremovable()
.types(PolicyEnforcerResolver.class)
.supplier(recorder.setup(oidcRunTimeConfig, keycloakConfig, httpConfiguration,
tlsRegistryBuildItem.registry()))
.scope(Singleton.class)
.setRuntimeInit()
.done();
}
return null;
}

public static class IsEnabled implements BooleanSupplier {
KeycloakPolicyEnforcerBuildTimeConfig config;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.keycloak.pep;

import org.keycloak.adapters.authorization.PolicyEnforcer;

import io.quarkus.oidc.OidcTenantConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

/**
* A {@link PolicyEnforcer} resolver.
*/
public interface PolicyEnforcerResolver {

Uni<PolicyEnforcer> resolvePolicyEnforcer(RoutingContext routingContext, OidcTenantConfig tenantConfig);

long getReadTimeout();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.keycloak.pep;

import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

/**
* A tenant resolver is responsible for resolving the {@link KeycloakPolicyEnforcerTenantConfig} for tenants, dynamically.
*/
public interface TenantPolicyConfigResolver {

/**
* Returns a {@link KeycloakPolicyEnforcerTenantConfig} given a {@code RoutingContext} and tenant id.
*
* @param routingContext routing context; nullable
* @param tenantConfig tenant config; never null
* @param requestContext request context; never null
*
* @return the tenant configuration. If the uni resolves to {@code null}, indicates that the default
* configuration/tenant should be chosen
*/
Uni<KeycloakPolicyEnforcerTenantConfig> resolve(RoutingContext routingContext, OidcTenantConfig tenantConfig,
OidcRequestContext<KeycloakPolicyEnforcerTenantConfig> requestContext);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.quarkus.keycloak.pep.runtime;

import static io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerUtil.createPolicyEnforcer;
import static io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerUtil.getOidcTenantConfig;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;

import org.keycloak.adapters.authorization.PolicyEnforcer;

import io.quarkus.arc.InjectableInstance;
import io.quarkus.keycloak.pep.PolicyEnforcerResolver;
import io.quarkus.keycloak.pep.TenantPolicyConfigResolver;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.runtime.BlockingTaskRunner;
import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@Singleton
public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver {

private final TenantPolicyConfigResolver dynamicConfigResolver;
private final BlockingTaskRunner<KeycloakPolicyEnforcerTenantConfig> requestContext;
private final Map<String, PolicyEnforcer> namedPolicyEnforcers;
private final PolicyEnforcer defaultPolicyEnforcer;
private final long readTimeout;
private final boolean globalTrustAll;

DefaultPolicyEnforcerResolver(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config,
HttpConfiguration httpConfiguration, BlockingSecurityExecutor blockingSecurityExecutor,
Instance<TenantPolicyConfigResolver> configResolver,
InjectableInstance<TlsConfigurationRegistry> tlsConfigRegistryInstance) {
this.readTimeout = httpConfiguration.readTimeout.toMillis();

if (tlsConfigRegistryInstance.isResolvable()) {
this.globalTrustAll = tlsConfigRegistryInstance.get().getDefault().map(TlsConfiguration::isTrustAll).orElse(false);
} else {
this.globalTrustAll = false;
}

this.defaultPolicyEnforcer = createPolicyEnforcer(oidcConfig.defaultTenant, config.defaultTenant(), globalTrustAll);
this.namedPolicyEnforcers = createNamedPolicyEnforcers(oidcConfig, config, globalTrustAll);
if (configResolver.isResolvable()) {
this.dynamicConfigResolver = configResolver.get();
this.requestContext = new BlockingTaskRunner<>(blockingSecurityExecutor);
} else {
this.dynamicConfigResolver = null;
this.requestContext = null;
}
}

@Override
public Uni<PolicyEnforcer> resolvePolicyEnforcer(RoutingContext routingContext, OidcTenantConfig tenantConfig) {
if (tenantConfig == null) {
return Uni.createFrom().item(defaultPolicyEnforcer);
}
if (dynamicConfigResolver == null) {
return Uni.createFrom().item(getStaticPolicyEnforcer(tenantConfig.tenantId.get()));
} else {
return getDynamicPolicyEnforcer(routingContext, tenantConfig)
.onItem().ifNull().continueWith(new Supplier<PolicyEnforcer>() {
@Override
public PolicyEnforcer get() {
return getStaticPolicyEnforcer(tenantConfig.tenantId.get());
}
});
}
}

@Override
public long getReadTimeout() {
return readTimeout;
}

PolicyEnforcer getStaticPolicyEnforcer(String tenantId) {
return tenantId != null && namedPolicyEnforcers.containsKey(tenantId)
? namedPolicyEnforcers.get(tenantId)
: defaultPolicyEnforcer;
}

boolean hasDynamicPolicyEnforcers() {
return dynamicConfigResolver != null;
}

private Uni<PolicyEnforcer> getDynamicPolicyEnforcer(RoutingContext routingContext, OidcTenantConfig config) {
return dynamicConfigResolver.resolve(routingContext, config, requestContext)
.onItem().ifNotNull().transform(new Function<KeycloakPolicyEnforcerTenantConfig, PolicyEnforcer>() {
@Override
public PolicyEnforcer apply(KeycloakPolicyEnforcerTenantConfig tenant) {
return createPolicyEnforcer(config, tenant, globalTrustAll);
}
});
}

private static Map<String, PolicyEnforcer> createNamedPolicyEnforcers(OidcConfig oidcConfig,
KeycloakPolicyEnforcerConfig config, boolean tlsConfigTrustAll) {
if (config.namedTenants().isEmpty()) {
return Map.of();
}

Map<String, PolicyEnforcer> policyEnforcerTenants = new HashMap<>();
for (Map.Entry<String, KeycloakPolicyEnforcerTenantConfig> tenant : config.namedTenants().entrySet()) {
OidcTenantConfig oidcTenantConfig = getOidcTenantConfig(oidcConfig, tenant.getKey());
policyEnforcerTenants.put(tenant.getKey(),
createPolicyEnforcer(oidcTenantConfig, tenant.getValue(), tlsConfigTrustAll));
}
return Map.copyOf(policyEnforcerTenants);
}
}
Loading