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

Move Keycloak Authorization Enforcer Tenant config to runtime and improve usability with aggregated policy enforcer paths #39512

Merged
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
16 changes: 7 additions & 9 deletions docs/src/main/asciidoc/security-keycloak-authorization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,15 @@ There's no need to deactivate policy checks for a Keycloak Authorization Policy

[source,properties]
----
quarkus.keycloak.policy-enforcer.paths.1.path=/api/public
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/public
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=DISABLED
----

To block access to the public resource to anonymous users, you can create an enforcing Keycloak Authorization Policy:

[source,properties]
----
quarkus.keycloak.policy-enforcer.paths.1.path=/api/public-enforcing
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/public-enforcing
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=ENFORCING
----

Expand All @@ -438,15 +438,13 @@ For example:
----
# path policy with enforced scope 'read' for method 'GET'
quarkus.keycloak.policy-enforcer.paths.1.name=Scope Permission Resource
quarkus.keycloak.policy-enforcer.paths.1.path=/api/protected/standard-way
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/protected/standard-way
quarkus.keycloak.policy-enforcer.paths.1.methods.get.method=GET
quarkus.keycloak.policy-enforcer.paths.1.methods.get.scopes=read <1>

# path policies without scope
quarkus.keycloak.policy-enforcer.paths.2.name=Scope Permission Resource
quarkus.keycloak.policy-enforcer.paths.2.path=/api/protected/programmatic-way
quarkus.keycloak.policy-enforcer.paths.3.name=Scope Permission Resource
quarkus.keycloak.policy-enforcer.paths.3.path=/api/protected/annotation-way
quarkus.keycloak.policy-enforcer.paths.2.paths=/api/protected/programmatic-way,/api/protected/annotation-way
----
<1> User must have resource permission 'Scope Permission Resource' and scope 'read'

Expand Down Expand Up @@ -532,7 +530,7 @@ quarkus.oidc.credentials.secret=secret

quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.policy-enforcer.paths.1.name=Permission Resource
quarkus.keycloak.policy-enforcer.paths.1.path=/api/permission
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/permission
quarkus.keycloak.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim

# Service Tenant
Expand All @@ -543,7 +541,7 @@ quarkus.oidc.service-tenant.credentials.secret=secret

quarkus.keycloak.service-tenant.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.name=Permission Resource Service
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.path=/api/permission
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.paths=/api/permission
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim


Expand All @@ -557,7 +555,7 @@ quarkus.oidc.webapp-tenant.roles.source=accesstoken

quarkus.keycloak.webapp-tenant.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.name=Permission Resource WebApp
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.path=/api/permission
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
----

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

import java.util.Map;
import java.util.function.BooleanSupplier;

import jakarta.inject.Singleton;
Expand All @@ -17,60 +16,29 @@
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.KeycloakPolicyEnforcerTenantConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.PathConfig;
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 {

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig, KeycloakPolicyEnforcerConfig config) {
RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig,
KeycloakPolicyEnforcerRecorder recorder,
KeycloakPolicyEnforcerConfig runtimeConfig) {
if (oidcBuildTimeConfig.enabled) {
if (isBodyHandlerRequired(config.defaultTenant)) {
return new RequireBodyHandlerBuildItem();
}
for (KeycloakPolicyEnforcerTenantConfig tenantConfig : config.namedTenants.values()) {
if (isBodyHandlerRequired(tenantConfig)) {
return new RequireBodyHandlerBuildItem();
}
}
return new RequireBodyHandlerBuildItem(recorder.createBodyHandlerRequiredEvaluator(runtimeConfig));
}
return null;
}

private static boolean isBodyHandlerRequired(KeycloakPolicyEnforcerTenantConfig config) {
if (isBodyClaimInformationPointDefined(config.policyEnforcer.claimInformationPoint.simpleConfig)) {
return true;
}
for (PathConfig path : config.policyEnforcer.paths.values()) {
if (isBodyClaimInformationPointDefined(path.claimInformationPoint.simpleConfig)) {
return true;
}
}
return false;
}

private static boolean isBodyClaimInformationPointDefined(Map<String, Map<String, String>> claims) {
for (Map.Entry<String, Map<String, String>> entry : claims.entrySet()) {
Map<String, String> value = entry.getValue();

for (String nestedValue : value.values()) {
if (nestedValue.contains("request.body")) {
return true;
}
}
}

return false;
}

@BuildStep
public AdditionalBeanBuildItem beans(OidcBuildTimeConfig oidcBuildTimeConfig, KeycloakPolicyEnforcerConfig config) {
public AdditionalBeanBuildItem beans(OidcBuildTimeConfig oidcBuildTimeConfig) {
if (oidcBuildTimeConfig.enabled) {
return AdditionalBeanBuildItem.builder().setUnremovable()
.addBeanClass(KeycloakPolicyEnforcerAuthorizer.class).build();
Expand All @@ -86,12 +54,12 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() {
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
public SyntheticBeanBuildItem setup(OidcBuildTimeConfig oidcBuildTimeConfig, OidcConfig oidcRunTimeConfig,
TlsConfig tlsConfig,
KeycloakPolicyEnforcerConfig keycloakConfig, KeycloakPolicyEnforcerRecorder recorder) {
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))
.supplier(recorder.setup(oidcRunTimeConfig, keycloakConfig, tlsConfig, httpConfiguration))
.scope(Singleton.class)
.setRuntimeInit()
.done();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;

import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.ClaimInformationPointConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.MethodConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.PathCacheConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.PathConfig;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
Expand All @@ -26,14 +33,26 @@

@Recorder
public class KeycloakPolicyEnforcerRecorder {
final HttpConfiguration httpConfiguration;

public KeycloakPolicyEnforcerRecorder(HttpConfiguration httpConfiguration) {
this.httpConfiguration = httpConfiguration;
public BooleanSupplier createBodyHandlerRequiredEvaluator(KeycloakPolicyEnforcerConfig config) {
return new BooleanSupplier() {
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
@Override
public boolean getAsBoolean() {
if (isBodyHandlerRequired(config.defaultTenant)) {
return true;
}
for (KeycloakPolicyEnforcerTenantConfig tenantConfig : config.namedTenants.values()) {
if (isBodyHandlerRequired(tenantConfig)) {
return true;
}
}
return false;
}
};
}

public Supplier<PolicyEnforcerResolver> setup(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config,
TlsConfig tlsConfig) {
TlsConfig tlsConfig, HttpConfiguration httpConfiguration) {
PolicyEnforcer defaultPolicyEnforcer = createPolicyEnforcer(oidcConfig.defaultTenant, config.defaultTenant, tlsConfig);
Map<String, PolicyEnforcer> policyEnforcerTenants = new HashMap<String, PolicyEnforcer>();
for (Map.Entry<String, KeycloakPolicyEnforcerTenantConfig> tenant : config.namedTenants.entrySet()) {
Expand Down Expand Up @@ -97,8 +116,7 @@ private static PolicyEnforcer createPolicyEnforcer(OidcTenantConfig oidcConfig,
adapterConfig.setProxyUrl(host + ":" + oidcConfig.proxy.port);
}

PolicyEnforcerConfig enforcerConfig = getPolicyEnforcerConfig(keycloakPolicyEnforcerConfig,
adapterConfig);
PolicyEnforcerConfig enforcerConfig = getPolicyEnforcerConfig(keycloakPolicyEnforcerConfig);

adapterConfig.setPolicyEnforcerConfig(enforcerConfig);

Expand Down Expand Up @@ -138,8 +156,7 @@ private static Map<String, Map<String, Object>> getClaimInformationPointConfig(C
return cipConfig;
}

private static PolicyEnforcerConfig getPolicyEnforcerConfig(KeycloakPolicyEnforcerTenantConfig config,
AdapterConfig adapterConfig) {
private static PolicyEnforcerConfig getPolicyEnforcerConfig(KeycloakPolicyEnforcerTenantConfig config) {
PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig();

enforcerConfig.setLazyLoadPaths(config.policyEnforcer.lazyLoadPaths);
Expand All @@ -155,29 +172,86 @@ private static PolicyEnforcerConfig getPolicyEnforcerConfig(KeycloakPolicyEnforc

enforcerConfig.setClaimInformationPointConfig(
getClaimInformationPointConfig(config.policyEnforcer.claimInformationPoint));
enforcerConfig.setPaths(config.policyEnforcer.paths.values().stream().map(
pathConfig -> {
PolicyEnforcerConfig.PathConfig config1 = new PolicyEnforcerConfig.PathConfig();

config1.setName(pathConfig.name.orElse(null));
config1.setPath(pathConfig.path.orElse(null));
config1.setEnforcementMode(pathConfig.enforcementMode);
config1.setMethods(pathConfig.methods.values().stream().map(
methodConfig -> {
PolicyEnforcerConfig.MethodConfig mConfig = new PolicyEnforcerConfig.MethodConfig();

mConfig.setMethod(methodConfig.method);
mConfig.setScopes(methodConfig.scopes);
mConfig.setScopesEnforcementMode(methodConfig.scopesEnforcementMode);

return mConfig;
}).collect(Collectors.toList()));
config1.setClaimInformationPointConfig(
getClaimInformationPointConfig(pathConfig.claimInformationPoint));

return config1;
enforcerConfig.setPaths(config.policyEnforcer.paths.values().stream().flatMap(
new Function<PathConfig, Stream<? extends PolicyEnforcerConfig.PathConfig>>() {
@Override
public Stream<? extends PolicyEnforcerConfig.PathConfig> apply(PathConfig pathConfig) {
var paths = getPathConfigPaths(pathConfig);
if (paths.isEmpty()) {
return Stream.of(createKeycloakPathConfig(pathConfig, null));
} else {
return paths.stream().map(new Function<String, PolicyEnforcerConfig.PathConfig>() {
@Override
public PolicyEnforcerConfig.PathConfig apply(String path) {
return createKeycloakPathConfig(pathConfig, path);
}
});
}
}
}).collect(Collectors.toList()));

return enforcerConfig;
}

private static Set<String> getPathConfigPaths(PathConfig pathConfig) {
Set<String> paths = new HashSet<>();
if (pathConfig.path.isPresent()) {
paths.add(pathConfig.path.get());
}
if (pathConfig.paths.isPresent()) {
paths.addAll(pathConfig.paths.get());
}
return paths;
}

private static PolicyEnforcerConfig.PathConfig createKeycloakPathConfig(PathConfig pathConfig, String path) {
PolicyEnforcerConfig.PathConfig config1 = new PolicyEnforcerConfig.PathConfig();

config1.setName(pathConfig.name.orElse(null));
config1.setPath(path);
config1.setEnforcementMode(pathConfig.enforcementMode);
config1.setMethods(pathConfig.methods.values().stream().map(
new Function<MethodConfig, PolicyEnforcerConfig.MethodConfig>() {
@Override
public PolicyEnforcerConfig.MethodConfig apply(MethodConfig methodConfig) {
PolicyEnforcerConfig.MethodConfig mConfig = new PolicyEnforcerConfig.MethodConfig();

mConfig.setMethod(methodConfig.method);
mConfig.setScopes(methodConfig.scopes);
mConfig.setScopesEnforcementMode(methodConfig.scopesEnforcementMode);

return mConfig;
}
}).collect(Collectors.toList()));
config1.setClaimInformationPointConfig(
getClaimInformationPointConfig(pathConfig.claimInformationPoint));
return config1;
}

private static boolean isBodyHandlerRequired(KeycloakPolicyEnforcerTenantConfig config) {
if (isBodyClaimInformationPointDefined(config.policyEnforcer.claimInformationPoint.simpleConfig)) {
return true;
}
for (PathConfig path : config.policyEnforcer.paths
.values()) {
if (isBodyClaimInformationPointDefined(path.claimInformationPoint.simpleConfig)) {
return true;
}
}
return false;
}

private static boolean isBodyClaimInformationPointDefined(Map<String, Map<String, String>> claims) {
for (Map.Entry<String, Map<String, String>> entry : claims.entrySet()) {
Map<String, String> value = entry.getValue();

for (String nestedValue : value.values()) {
if (nestedValue.contains("request.body")) {
return true;
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ public static class PathConfig {
/**
* A URI relative to the application’s context path that should be protected by the policy enforcer
*/
@Deprecated(since = "Quarkus 3.10") // use the 'paths' configuration property
@ConfigItem
public Optional<String> path;

/**
* HTTP request paths that should be protected by the policy enforcer
*/
@ConfigItem
public Optional<List<String>> paths;

/**
* The HTTP methods (for example, GET, POST, PATCH) to protect and how they are associated with the scopes for a
* given
Expand Down
Loading
Loading