Skip to content

Commit

Permalink
Add Keycloak Authorization dynamic tenant config resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Mar 28, 2024
1 parent b18f8d6 commit 96f0b5c
Show file tree
Hide file tree
Showing 21 changed files with 1,409 additions and 359 deletions.
56 changes: 56 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,62 @@ quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.paths=/api/permission
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.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) {
var builder = KeycloakPolicyEnforcerTenantConfig.builder(enforcerConfig.defaultTenant()); <1>
var path = builder.setPaths("/enhanced-config");
path.setPermissionName("Permission Name");
path.setGet("read-scope");
this.enhancedTenantConfig = builder.build();
this.newTenantConfig = builder.setPaths("/new-config") <2>
.setClaimInformationPoint(Map.of("claims", Map.of("grant", "{request.parameter['grant']}")))
.build();
}
@Override
public Uni<KeycloakPolicyEnforcerTenantConfig> resolve(RoutingContext routingContext, String tenantId,
KeycloakRequestContext requestContext) {
String path = routingContext.normalizedPath();
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.runtime.TlsConfig;
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,8 @@ RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig,
public AdditionalBeanBuildItem beans(OidcBuildTimeConfig oidcBuildTimeConfig) {
if (oidcBuildTimeConfig.enabled) {
return AdditionalBeanBuildItem.builder().setUnremovable()
.addBeanClass(KeycloakPolicyEnforcerAuthorizer.class).build();
.addBeanClasses(KeycloakPolicyEnforcerAuthorizer.class, DefaultPolicyEnforcerResolver.class)
.build();
}
return null;
}
Expand All @@ -51,22 +46,6 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() {
return new ExtensionSslNativeSupportBuildItem(Feature.KEYCLOAK_AUTHORIZATION);
}

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
public SyntheticBeanBuildItem setup(OidcBuildTimeConfig oidcBuildTimeConfig, OidcConfig oidcRunTimeConfig,
TlsConfig tlsConfig, KeycloakPolicyEnforcerConfig keycloakConfig, KeycloakPolicyEnforcerRecorder recorder,
HttpConfiguration httpConfiguration) {
if (oidcBuildTimeConfig.enabled) {
return SyntheticBeanBuildItem.configure(PolicyEnforcerResolver.class).unremovable()
.types(PolicyEnforcerResolver.class)
.supplier(recorder.setup(oidcRunTimeConfig, keycloakConfig, tlsConfig, httpConfiguration))
.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,17 @@
package io.quarkus.keycloak.pep;

import org.keycloak.adapters.authorization.PolicyEnforcer;

import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

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

Uni<PolicyEnforcer> resolvePolicyEnforcer(RoutingContext routingContext, String tenantId);

long getReadTimeout();

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

import java.util.function.Supplier;

import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig;
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 the routing context
* @param tenantId tenant id
* @param requestContext request context
*
* @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, String tenantId,
KeycloakRequestContext requestContext);

/**
* Keycloak Context that can be used to run blocking tasks.
*/
interface KeycloakRequestContext {
<T> Uni<T> runBlocking(Supplier<T> function);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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.keycloak.pep.PolicyEnforcerResolver;
import io.quarkus.keycloak.pep.TenantPolicyConfigResolver;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
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 TenantPolicyConfigResolver.KeycloakRequestContext requestContext;
private final Map<String, PolicyEnforcer> namedPolicyEnforcers;
private final PolicyEnforcer defaultPolicyEnforcer;
private final long readTimeout;
private final boolean tlsConfigTrustAll;
private final OidcConfig oidcConfig;

DefaultPolicyEnforcerResolver(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config, TlsConfig tlsConfig,
HttpConfiguration httpConfiguration, BlockingSecurityExecutor blockingSecurityExecutor,
Instance<TenantPolicyConfigResolver> configResolver) {
this.readTimeout = httpConfiguration.readTimeout.toMillis();
this.oidcConfig = oidcConfig;
this.tlsConfigTrustAll = tlsConfig.trustAll;
this.defaultPolicyEnforcer = createPolicyEnforcer(oidcConfig.defaultTenant, config.defaultTenant(), tlsConfigTrustAll);
this.namedPolicyEnforcers = createNamedPolicyEnforcers(oidcConfig, config, tlsConfigTrustAll);
if (configResolver.isResolvable()) {
this.dynamicConfigResolver = configResolver.get();
this.requestContext = createKeycloakRequestContext(blockingSecurityExecutor);
} else {
this.dynamicConfigResolver = null;
this.requestContext = null;
}
}

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

@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, String tenantId) {
return dynamicConfigResolver.resolve(routingContext, tenantId, requestContext)
.onItem().ifNotNull().transform(new Function<KeycloakPolicyEnforcerTenantConfig, PolicyEnforcer>() {
@Override
public PolicyEnforcer apply(KeycloakPolicyEnforcerTenantConfig tenant) {
return createPolicyEnforcer(tenant, tlsConfigTrustAll, tenantId, oidcConfig);
}
});
}

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);
}

private static TenantPolicyConfigResolver.KeycloakRequestContext createKeycloakRequestContext(
BlockingSecurityExecutor blockingSecurityExecutor) {
return new TenantPolicyConfigResolver.KeycloakRequestContext() {
@Override
public <T> Uni<T> runBlocking(Supplier<T> function) {
return blockingSecurityExecutor.executeBlocking(function);
}
};
}
}
Loading

0 comments on commit 96f0b5c

Please sign in to comment.