keycloak/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java

666 lines
26 KiB
Java

/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.adapters.authorization;
import static org.keycloak.adapters.authorization.util.JsonUtils.asAccessToken;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ServiceLoader;
import java.util.Set;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.spi.HttpResponse;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.PermissionResource;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.common.util.Base64;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.util.JsonSerialization;
/**
* <p>A Policy Enforcement Point (PEP) that requests and enforces authorization decisions from Keycloak.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private static final String HTTP_METHOD_DELETE = "DELETE";
public static Builder builder() {
return new Builder();
}
private final AuthzClient authzClient;
private final Map<String, PathConfig> paths;
private final PathConfigMatcher pathMatcher;
private final HttpClient httpClient;
private final PolicyEnforcerConfig enforcerConfig;
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
protected PolicyEnforcer(Builder builder) {
enforcerConfig = builder.getEnforcerConfig();
Configuration authzClientConfig = builder.authzClientConfig;
if (authzClientConfig.getRealm() == null) {
authzClientConfig.setRealm(enforcerConfig.getRealm());
}
if (authzClientConfig.getAuthServerUrl() == null) {
authzClientConfig.setAuthServerUrl(enforcerConfig.getAuthServerUrl());
}
if (authzClientConfig.getCredentials() == null || authzClientConfig.getCredentials().isEmpty()) {
authzClientConfig.setCredentials(enforcerConfig.getCredentials());
}
if (authzClientConfig.getResource() == null) {
authzClientConfig.setResource(enforcerConfig.getResource());
}
authzClient = AuthzClient.create(authzClientConfig);
httpClient = authzClient.getConfiguration().getHttpClient();
pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient);
paths = pathMatcher.getPathConfig();
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
}
public AuthorizationContext enforce(HttpRequest request, HttpResponse response) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", request.getURI());
}
AuthorizationContext context = authorize(request, response);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED");
LOGGER.debugv("Returning authorization context with permissions:");
for (Permission permission : context.getPermissions()) {
LOGGER.debug(permission);
}
}
return context;
}
public HttpClient getHttpClient() {
return httpClient;
}
public Map<String, PathConfig> getPaths() {
return Collections.unmodifiableMap(paths);
}
public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
return claimInformationPointProviderFactories;
}
public PathConfigMatcher getPathMatcher() {
return pathMatcher;
}
private AuthorizationContext authorize(HttpRequest request, HttpResponse response) {
EnforcementMode enforcementMode = enforcerConfig.getEnforcementMode();
TokenPrincipal principal = request.getPrincipal();
boolean anonymous = principal == null || principal.getRawToken() == null;
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
if (anonymous) {
response.sendError(401, "Invalid bearer");
}
return createEmptyAuthorizationContext(true);
}
PathConfig pathConfig = getPathConfig(request);
if (anonymous) {
if (!isDefaultAccessDeniedUri(request)) {
if (pathConfig != null) {
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createEmptyAuthorizationContext(true);
} else {
challenge(pathConfig, getRequiredScopes(pathConfig, request), request, response);
}
} else {
handleAccessDenied(response);
}
}
return createEmptyAuthorizationContext(false);
}
AccessToken accessToken = principal.getToken();
if (accessToken != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
}
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken, null);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
}
if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
}
handleAccessDenied(response);
return createEmptyAuthorizationContext(false);
}
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createAuthorizationContext(accessToken, pathConfig);
}
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
Map<String, List<String>> claims = resolveClaims(pathConfig, request);
if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
AccessToken original = accessToken;
accessToken = requestAuthorizationToken(pathConfig, methodConfig, request, claims);
if (accessToken != null) {
AccessToken.Authorization authorization = original.getAuthorization();
if (authorization == null) {
authorization = new AccessToken.Authorization();
authorization.setPermissions(new ArrayList<Permission>());
}
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
Collection<Permission> grantedPermissions = authorization.getPermissions();
Collection<Permission> newPermissions = newAuthorization.getPermissions();
for (Permission newPermission : newPermissions) {
if (!grantedPermissions.contains(newPermission)) {
grantedPermissions.add(newPermission);
}
}
}
original.setAuthorization(authorization);
if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
}
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
}
if (!challenge(pathConfig, methodConfig, request, response)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
}
handleAccessDenied(response);
}
}
return createEmptyAuthorizationContext(false);
}
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, HttpRequest request, Map<String, List<String>> claims) {
if (isDefaultAccessDeniedUri(request)) {
return true;
}
Authorization authorization = accessToken.getAuthorization();
if (authorization == null) {
return false;
}
boolean hasPermission = false;
Collection<Permission> grantedPermissions = authorization.getPermissions();
for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
continue;
}
if (hasResourceScopePermission(methodConfig, permission)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
}
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
pathMatcher.removeFromCache(getPath(request));
}
return hasValidClaims(permission, claims);
}
}
} else {
if (hasResourceScopePermission(methodConfig, permission)) {
return true;
}
}
}
if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
return true;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
}
return false;
}
protected Map<String, List<String>> resolveClaims(PathConfig pathConfig, HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
resolveClaims(claims, enforcerConfig.getClaimInformationPointConfig(), request);
resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), request);
return claims;
}
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, HttpResponse response) {
if (isBearerAuthorization(request)) {
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, request);
if (ticket != null) {
response.sendError(401);
response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
.append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
} else {
response.sendError(403);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending challenge");
}
return true;
}
handleAccessDenied(response);
return true;
}
protected void handleAccessDenied(HttpResponse response) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
if (accessDeniedPath != null) {
response.sendError(302);
response.setHeader("Location", accessDeniedPath);
} else {
response.sendError(403);
}
}
private boolean hasValidClaims(Permission permission, Map<String, List<String>> claims) {
Map<String, Set<String>> grantedClaims = permission.getClaims();
if (grantedClaims != null) {
if (claims.isEmpty()) {
return false;
}
for (Entry<String, Set<String>> entry : grantedClaims.entrySet()) {
List<String> requestClaims = claims.get(entry.getKey());
if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
return false;
}
}
}
return true;
}
private boolean isDefaultAccessDeniedUri(HttpRequest request) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
List<String> requiredScopes = methodConfig.getScopes();
Set<String> allowedScopes = permission.getScopes();
if (allowedScopes.isEmpty()) {
return true;
}
PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
return allowedScopes.containsAll(requiredScopes);
}
if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
for (String requiredScope : requiredScopes) {
if (allowedScopes.contains(requiredScope)) {
return true;
}
}
}
return requiredScopes.isEmpty();
}
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
return new ClientAuthorizationContext(authzClient) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
}
@Override
public boolean hasResourcePermission(String resourceName) {
return granted;
}
@Override
public boolean hasScopePermission(String scopeName) {
return granted;
}
@Override
public List<Permission> getPermissions() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isGranted() {
return granted;
}
};
}
private String getPath(HttpRequest request) {
return request.getRelativePath();
}
private MethodConfig getRequiredScopes(PathConfig pathConfig, HttpRequest request) {
String method = request.getMethod();
for (MethodConfig methodConfig : pathConfig.getMethods()) {
if (methodConfig.getMethod().equals(method)) {
return methodConfig;
}
}
MethodConfig methodConfig = new MethodConfig();
methodConfig.setMethod(request.getMethod());
List scopes = new ArrayList<>();
if (Boolean.TRUE.equals(enforcerConfig.getHttpMethodAsScope())) {
scopes.add(request.getMethod());
} else {
scopes.addAll(pathConfig.getScopes());
}
methodConfig.setScopes(scopes);
methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
return methodConfig;
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
return new ClientAuthorizationContext(accessToken, pathConfig, authzClient);
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
// first we try a match using resource id
boolean resourceMatch = matchResourcePermission(actualPathConfig, permission);
// as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
if (!resourceMatch && actualPathConfig.isInstance()) {
resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission);
}
return resourceMatch;
}
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
return permission.getResourceId().equals(actualPathConfig.getId());
}
private PathConfig getPathConfig(HttpRequest request) {
return isDefaultAccessDeniedUri(request) ? null : pathMatcher.matches(getPath(request));
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, Map<String, List<String>> claims) {
if (enforcerConfig.getUserManagedAccess() != null) {
return null;
}
try {
TokenPrincipal principal = request.getPrincipal();
String accessTokenString = principal.getRawToken();
AccessToken accessToken = principal.getToken();
AuthorizationRequest authzRequest = new AuthorizationRequest();
if (isBearerAuthorization(request) || accessToken.getAuthorization() != null) {
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
}
if (!claims.isEmpty()) {
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
}
if (accessToken.getAuthorization() != null) {
authzRequest.setRpt(accessTokenString);
}
LOGGER.debug("Obtaining authorization for authenticated user.");
AuthorizationResponse authzResponse;
if (isBearerAuthorization(request)) {
authzRequest.setSubjectToken(accessTokenString);
authzResponse = authzClient.authorization().authorize(authzRequest);
} else {
authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
}
if (authzResponse != null) {
return asAccessToken(authzResponse.getToken());
}
} catch (AuthorizationDeniedException ignore) {
LOGGER.debug("Authorization denied", ignore);
} catch (Exception e) {
LOGGER.debug("Authorization failed", e);
}
return null;
}
private String getPermissionTicket(PathConfig pathConfig, MethodConfig methodConfig, AuthzClient authzClient, HttpRequest httpFacade) {
if (enforcerConfig.getUserManagedAccess() != null) {
ProtectionResource protection = authzClient.protection();
PermissionResource permission = protection.permission();
PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceId(pathConfig.getId());
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
if (!claims.isEmpty()) {
permissionRequest.setClaims(claims);
}
return permission.create(permissionRequest).getTicket();
}
return null;
}
private boolean isBearerAuthorization(HttpRequest request) {
List<String> authHeaders = request.getHeaders("Authorization");
if (authHeaders != null) {
for (String authHeader : authHeaders) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) continue;
if (!split[0].equalsIgnoreCase("Bearer")) continue;
return true;
}
}
return authzClient.getConfiguration().isBearerOnly();
}
private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
for (ClaimInformationPointProviderFactory factory : loader) {
factory.init(this);
claimInformationPointProviderFactories.put(factory.getName(), factory);
}
}
private void resolveClaims(Map<String, List<String>> claims, Map<String, Map<String, Object>> claimInformationPointConfig, HttpRequest request) {
if (claimInformationPointConfig != null) {
for (Entry<String, Map<String, Object>> claimDef : claimInformationPointConfig.entrySet()) {
ClaimInformationPointProviderFactory factory = claimInformationPointProviderFactories.get(claimDef.getKey());
if (factory != null) {
claims.putAll(factory.create(claimDef.getValue()).resolve(request));
}
}
}
}
public static class Builder {
Configuration authzClientConfig = new Configuration();
private Builder() {
}
public Builder authServerUrl(String authServerUrl) {
authzClientConfig.setAuthServerUrl(authServerUrl);
return this;
}
public Builder realm(String realm) {
authzClientConfig.setRealm(realm);
return this;
}
public Builder clientId(String clientId) {
authzClientConfig.setResource(clientId);
return this;
}
public Builder bearerOnly(boolean bearerOnly) {
authzClientConfig.setBearerOnly(bearerOnly);
return this;
}
public Builder credentials(Map<String, Object> credentials) {
authzClientConfig.setCredentials(credentials);
return this;
}
public Builder enforcerConfig(PolicyEnforcerConfig enforcerConfig) {
authzClientConfig.setPolicyEnforcerConfig(enforcerConfig);
return this;
}
public Builder enforcerConfig(InputStream is) {
try {
enforcerConfig(JsonSerialization.readValue(is, PolicyEnforcerConfig.class));
} catch (Exception cause) {
throw new RuntimeException("Failed to read configuration", cause);
}
return this;
}
public Builder httpClient(HttpClient httpClient) {
authzClientConfig.setHttpClient(httpClient);
return this;
}
public Builder credentialProvider(ClientCredentialsProvider credentialsProvider) {
authzClientConfig.setClientCredentialsProvider(credentialsProvider);
return this;
}
public PolicyEnforcer build() {
return new PolicyEnforcer(this);
}
PolicyEnforcerConfig getEnforcerConfig() {
return authzClientConfig.getPolicyEnforcerConfig();
}
}
}