Removing policy-enforcer from Keycloak repository

closes #32191

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2024-08-16 11:07:57 +02:00 committed by Bruno Oliveira da Silva
parent 967893d3cf
commit cd947ce3bc
54 changed files with 12 additions and 5092 deletions

View file

@ -1,54 +0,0 @@
<?xml version="1.0"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-parent</artifactId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-policy-enforcer-tests</artifactId>
<name>Keycloak Authz: Policy Enforcer for tests</name>
<description>Keycloak Policy Enforcer. This module is supposed to be used just in the Keycloak repository for the testsuite. It is NOT supposed to be used by the 3rd party applications.
For the use by 3rd party applications, please use `org.keycloak:keycloak-policy-enforcer` module.</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client-tests</artifactId>
</dependency>
<!-- Built-in Elytron/Jakarta Servlet integration -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-http-oidc</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View file

@ -1,203 +0,0 @@
/*
* 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 java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import org.keycloak.common.util.Time;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/**
* A simple LRU cache implementation supporting expiration and maximum number of entries.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathCache {
/**
* The load factor.
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private final Map<String, CacheEntry> cache;
private final AtomicBoolean writing = new AtomicBoolean(false);
private final long maxAge;
private final boolean enabled;
private final Map<String, PathConfig> paths;
/**
* Creates a new instance.
* @param maxEntries the maximum number of entries to keep in the cache
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
* @param paths the pre-configured paths
*/
PathCache(final int maxEntries, long maxAge,
Map<String, PathConfig> paths) {
cache = Collections.synchronizedMap(new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return cache.size() > maxEntries;
}
});
this.maxAge = maxAge;
this.enabled = ! (maxAge < -1 || (maxAge > -1 && maxAge <= 0));
this.paths = paths;
}
public void put(String uri, PathConfig newValue) {
if (!enabled) {
if (newValue != null) {
// if disabled we also remove from the pre-defined paths map
markForInvalidation(newValue);
}
return;
}
try {
if (parkForWriteAndCheckInterrupt()) {
return;
}
CacheEntry cacheEntry = cache.get(uri);
if (cacheEntry == null) {
cache.put(uri, new CacheEntry(uri, newValue, maxAge));
}
} finally {
writing.lazySet(false);
}
}
private void markForInvalidation(PathConfig newValue) {
PathConfig pathConfig = paths.get(newValue.getPath());
if (pathConfig != null && !pathConfig.isStatic()) {
// invalidate the configuration so that the path config is reload based on latest changes on the server
pathConfig.invalidate();
}
}
public boolean containsKey(String uri) {
return cache.containsKey(uri);
}
public PathConfig get(String uri) {
if (parkForReadAndCheckInterrupt()) {
return null;
}
CacheEntry cached = cache.get(uri);
if (cached != null) {
return removeIfExpired(cached);
}
return null;
}
public void remove(String key) {
try {
if (parkForWriteAndCheckInterrupt()) {
return;
}
cache.remove(key);
} finally {
writing.lazySet(false);
}
}
private PathConfig removeIfExpired(CacheEntry cached) {
if (cached == null) {
return null;
}
PathConfig config = cached.value();
if (cached.isExpired()) {
remove(cached.key());
if (config != null && config.getPath() != null) {
// also remove from pre-defined paths map so that changes on the server are properly reflected
markForInvalidation(config);
}
return null;
}
return config;
}
private boolean parkForWriteAndCheckInterrupt() {
while (!writing.compareAndSet(false, true)) {
LockSupport.parkNanos(1L);
if (Thread.interrupted()) {
return true;
}
}
return false;
}
private boolean parkForReadAndCheckInterrupt() {
while (writing.get()) {
LockSupport.parkNanos(1L);
if (Thread.interrupted()) {
return true;
}
}
return false;
}
public int size() {
return cache.size();
}
private static final class CacheEntry {
final String key;
final PathConfig value;
final long expiration;
CacheEntry(String key, PathConfig value, long maxAge) {
this.key = key;
this.value = value;
if(maxAge == -1) {
expiration = -1;
} else {
expiration = Time.currentTimeMillis() + maxAge;
}
}
String key() {
return key;
}
PathConfig value() {
return value;
}
boolean isExpired() {
return expiration != -1 ? Time.currentTimeMillis() > expiration : false;
}
}
}

View file

@ -1,293 +0,0 @@
/*
* Copyright 2023 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 java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathConfigMatcher extends PathMatcher<PathConfig> {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private final Map<String, PathConfig> paths;
private final PathCache pathCache;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
PathConfigMatcher(PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
this.enforcerConfig = enforcerConfig;
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
if (cacheConfig == null) {
cacheConfig = new PathCacheConfig();
}
this.authzClient = authzClient;
this.paths = configurePaths();
this.pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configuration:");
for (PathConfig pathConfig : this.paths.values()) {
LOGGER.debug(pathConfig);
}
}
}
@Override
public PathConfig matches(String targetUri) {
PathConfig pathConfig = pathCache.get(targetUri);
if (pathCache.containsKey(targetUri) || pathConfig != null) {
return pathConfig;
}
pathConfig = super.matches(targetUri);
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) {
try {
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
if (matchingResources.isEmpty()) {
// if this config is invalidated (e.g.: due to cache expiration) we remove and return null
if (pathConfig != null && pathConfig.isInvalidated()) {
paths.remove(targetUri);
return null;
}
} else {
Map<String, Map<String, Object>> cipConfig = null;
PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING;
ResourceRepresentation targetResource = matchingResources.get(0);
List<org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig> methodConfig = null;
boolean isStatic = false;
if (pathConfig != null) {
cipConfig = pathConfig.getClaimInformationPointConfig();
enforcementMode = pathConfig.getEnforcementMode();
methodConfig = pathConfig.getMethods();
isStatic = pathConfig.isStatic();
} else {
for (PathConfig existingPath : paths.values()) {
if (targetResource.getId().equals(existingPath.getId())
&& existingPath.isStatic()
&& !org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) {
return null;
}
}
}
pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next();
if (cipConfig != null) {
pathConfig.setClaimInformationPointConfig(cipConfig);
}
if (methodConfig != null) {
pathConfig.setMethods(methodConfig);
}
pathConfig.setStatic(isStatic);
pathConfig.setEnforcementMode(enforcementMode);
}
} catch (Exception cause) {
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
return null;
}
}
}
pathCache.put(targetUri, pathConfig);
return pathConfig;
}
@Override
protected String getPath(PathConfig entry) {
return entry.getPath();
}
@Override
protected Collection<PathConfig> getPaths() {
return paths.values();
}
public PathCache getPathCache() {
return pathCache;
}
@Override
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = authzClient.protection().resource();
// search by an exact match
List<ResourceRepresentation> search = resource.findByUri(path);
// if exact match not found, try to obtain from current path the parent path.
// if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
// this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
// so any permission granted to parent also applies to sub resources
if (search.isEmpty()) {
search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
}
if (!search.isEmpty()) {
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next();
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
return config;
}
}
return null;
}
public void removeFromCache(String pathConfig) {
pathCache.remove(pathConfig);
}
public Map<String, PathConfig> getPathConfig() {
return paths;
}
private Map<String, PathConfig> configurePaths() {
ProtectedResource protectedResource = this.authzClient.protection().resource();
boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths();
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
if (!org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
loadPathsFromServer = false;
break;
}
}
if (loadPathsFromServer) {
LOGGER.info("No path provided in configuration.");
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
return paths;
} else {
LOGGER.info("Paths provided in configuration.");
return configureDefinedPaths(protectedResource, enforcerConfig);
}
}
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
ResourceRepresentation resource;
String resourceName = pathConfig.getName();
String path = pathConfig.getPath();
if (resourceName != null) {
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
resource = protectedResource.findByName(resourceName);
} else {
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
if (resources.isEmpty()) {
resources = protectedResource.findByMatchingUri(path);
}
if (resources.size() == 1) {
resource = resources.get(0);
} else if (resources.size() > 1) {
throw new RuntimeException("Multiple resources found with the same uri");
} else {
resource = null;
}
}
if (resource != null) {
pathConfig.setId(resource.getId());
// if the resource is statically bound to a resource it means the config can not be invalidated
if (resourceName != null) {
pathConfig.setStatic(true);
}
}
if (org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
pathConfig.setStatic(true);
}
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
if (current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
}
}
if (existingPath == null) {
paths.put(pathConfig.getPath(), pathConfig);
} else {
existingPath.getMethods().addAll(pathConfig.getMethods());
existingPath.getScopes().addAll(pathConfig.getScopes());
}
}
return paths;
}
private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
LOGGER.info("Querying the server for all resources associated with this application.");
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
if (!enforcerConfig.getLazyLoadPaths()) {
for (String id : protectedResource.findAll()) {
ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
paths.put(pathConfig.getPath(), pathConfig);
}
}
}
}
return paths;
}
}

View file

@ -1,669 +0,0 @@
/*
* 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 AuthzClient getAuthzClient() {
return authzClient;
}
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.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());
response.sendError(401);
} 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.setHeader("Location", accessDeniedPath);
response.sendError(302);
} 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();
}
}
}

View file

@ -1,56 +0,0 @@
/*
* Copyright 2023 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 java.security.Principal;
import org.keycloak.adapters.authorization.util.JsonUtils;
import org.keycloak.representations.AccessToken;
/**
* A {@link Principal} backed by a token representing the entity requesting permissions.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface TokenPrincipal extends Principal {
/**
* The token in its raw format.
*
* @return the token in its raw format.
*/
String getRawToken();
/**
* The {@link AccessToken} representation of {@link TokenPrincipal#getRawToken()}.
*
* @return the access token representation
*/
default AccessToken getToken() {
return JsonUtils.asAccessToken(getRawToken());
}
/**
* The name of the entity represented by the token.
*
* @return the name of the principal
*/
default String getName() {
return getToken().getPreferredUsername();
}
}

View file

@ -1,74 +0,0 @@
/*
* Copyright 2018 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.cip;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.PlaceHolders;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ClaimsInformationPointProvider implements ClaimInformationPointProvider {
private final Map<String, Object> config;
public ClaimsInformationPointProvider(Map<String, Object> config) {
this.config = config;
}
@Override
public Map<String, List<String>> resolve(HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
for (Entry<String, Object> configEntry : config.entrySet()) {
String claimName = configEntry.getKey();
Object claimValue = configEntry.getValue();
List<String> values = new ArrayList<>();
if (claimValue instanceof String) {
values = getValues(claimValue.toString(), request);
} else if (claimValue instanceof Collection) {
for (Object value : Collection.class.cast(claimValue)) {
List<String> resolvedValues = getValues(value.toString(), request);
if (!resolvedValues.isEmpty()) {
values.addAll(resolvedValues);
}
}
}
if (!values.isEmpty()) {
claims.put(claimName, values);
}
}
return claims;
}
private List<String> getValues(String value, HttpRequest httpFacade) {
return PlaceHolders.resolve(value, httpFacade);
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright 2018 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.cip;
import java.util.Map;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ClaimsInformationPointProviderFactory implements ClaimInformationPointProviderFactory<ClaimsInformationPointProvider> {
@Override
public String getName() {
return "claims";
}
@Override
public ClaimsInformationPointProvider create(Map<String, Object> config) {
return new ClaimsInformationPointProvider(config);
}
}

View file

@ -1,207 +0,0 @@
/*
* Copyright 2018 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.cip;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.JsonUtils;
import org.keycloak.adapters.authorization.util.PlaceHolders;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpClaimInformationPointProvider implements ClaimInformationPointProvider {
private final Map<String, Object> config;
private final HttpClient httpClient;
public HttpClaimInformationPointProvider(Map<String, Object> config, HttpClient httpClient) {
this.config = config;
this.httpClient = httpClient;
}
@Override
public Map<String, List<String>> resolve(HttpRequest request) {
try {
InputStream responseStream = executeRequest(request);
try (InputStream inputStream = new BufferedInputStream(responseStream)) {
JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
Map<String, List<String>> claims = new HashMap<>();
Map<String, Object> claimsDef = (Map<String, Object>) config.get("claims");
if (claimsDef == null) {
Iterator<String> nodeNames = jsonNode.fieldNames();
while (nodeNames.hasNext()) {
String nodeName = nodeNames.next();
claims.put(nodeName, JsonUtils.getValues(jsonNode.get(nodeName)));
}
} else {
for (Entry<String, Object> claimDef : claimsDef.entrySet()) {
List<String> jsonPaths = new ArrayList<>();
if (claimDef.getValue() instanceof Collection) {
jsonPaths.addAll(Collection.class.cast(claimDef.getValue()));
} else {
jsonPaths.add(claimDef.getValue().toString());
}
List<String> claimValues = new ArrayList<>();
for (String path : jsonPaths) {
claimValues.addAll(JsonUtils.getValues(jsonNode, path));
}
claims.put(claimDef.getKey(), claimValues);
}
}
return claims;
}
} catch (IOException cause) {
throw new RuntimeException("Could not obtain claims from http claim information point [" + config.get("url") + "] response", cause);
}
}
private InputStream executeRequest(HttpRequest request) {
String method = config.get("method").toString();
if (method == null) {
method = "GET";
}
RequestBuilder builder = null;
if ("GET".equalsIgnoreCase(method)) {
builder = RequestBuilder.get();
} else {
builder = RequestBuilder.post();
}
builder.setUri(config.get("url").toString());
byte[] bytes = new byte[0];
try {
setParameters(builder, request);
if (config.containsKey("headers")) {
setHeaders(builder, request);
}
HttpResponse response = httpClient.execute(builder.build());
HttpEntity entity = response.getEntity();
if (entity != null) {
bytes = EntityUtils.toByteArray(entity);
}
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
if (statusCode < 200 || statusCode >= 300) {
throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes);
}
return new ByteArrayInputStream(bytes);
} catch (Exception cause) {
try {
throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + StreamUtil.readString(new ByteArrayInputStream(bytes), Charset.forName("UTF-8")), cause);
} catch (Exception e) {
throw new RuntimeException("Error executing http method [" + builder + "]", cause);
}
}
}
private void setHeaders(RequestBuilder builder, HttpRequest request) {
Object headersDef = config.get("headers");
if (headersDef != null) {
Map<String, Object> headers = Map.class.cast(headersDef);
for (Entry<String, Object> header : headers.entrySet()) {
Object value = header.getValue();
List<String> headerValues = new ArrayList<>();
if (value instanceof Collection) {
Collection values = Collection.class.cast(value);
for (Object item : values) {
headerValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
headerValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String headerValue : headerValues) {
builder.addHeader(header.getKey(), headerValue);
}
}
}
}
private void setParameters(RequestBuilder builder, HttpRequest request) {
Object config = this.config.get("parameters");
if (config != null) {
Map<String, Object> paramsDef = Map.class.cast(config);
for (Entry<String, Object> paramDef : paramsDef.entrySet()) {
Object value = paramDef.getValue();
List<String> paramValues = new ArrayList<>();
if (value instanceof Collection) {
Collection values = Collection.class.cast(value);
for (Object item : values) {
paramValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
paramValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String paramValue : paramValues) {
builder.addParameter(paramDef.getKey(), paramValue);
}
}
}
}
}

View file

@ -1,45 +0,0 @@
/*
* Copyright 2018 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.cip;
import java.util.Map;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory<HttpClaimInformationPointProvider> {
private PolicyEnforcer policyEnforcer;
@Override
public String getName() {
return "http";
}
@Override
public void init(PolicyEnforcer policyEnforcer) {
this.policyEnforcer = policyEnforcer;
}
@Override
public HttpClaimInformationPointProvider create(Map<String, Object> config) {
return new HttpClaimInformationPointProvider(config, policyEnforcer.getHttpClient());
}
}

View file

@ -1,30 +0,0 @@
/*
* Copyright 2023 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.cip.spi;
import java.util.List;
import java.util.Map;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ClaimInformationPointProvider {
Map<String, List<String>> resolve(HttpRequest request);
}

View file

@ -1,35 +0,0 @@
/*
* Copyright 2023 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.cip.spi;
import java.util.Map;
import org.keycloak.adapters.authorization.PolicyEnforcer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ClaimInformationPointProviderFactory<C extends ClaimInformationPointProvider> {
String getName();
default void init(PolicyEnforcer policyEnforcer) {
}
C create(Map<String, Object> config);
}

View file

@ -1,53 +0,0 @@
package org.keycloak.adapters.authorization.integration.elytron;
import java.security.Principal;
import jakarta.servlet.http.HttpServletRequest;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.integration.jakarta.ServletPolicyEnforcerFilter;
import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.wildfly.security.http.oidc.OidcClientConfiguration;
import org.wildfly.security.http.oidc.OidcPrincipal;
import org.wildfly.security.http.oidc.RefreshableOidcSecurityContext;
public class ElytronPolicyEnforcerFilter extends ServletPolicyEnforcerFilter {
public ElytronPolicyEnforcerFilter(ConfigurationResolver configResolver) {
super(configResolver);
}
@Override
protected String extractBearerToken(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
if (principal == null) {
return null;
}
OidcPrincipal oidcPrincipal = (OidcPrincipal) principal;
RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) oidcPrincipal.getOidcSecurityContext();
if (securityContext == null) {
return null;
}
return securityContext.getTokenString();
}
@Override
protected PolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, PolicyEnforcerConfig enforcerConfig) {
RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) ((OidcPrincipal) servletRequest.getUserPrincipal()).getOidcSecurityContext();
OidcClientConfiguration configuration = securityContext.getOidcClientConfiguration();
String authServerUrl = configuration.getAuthServerBaseUrl();
return PolicyEnforcer.builder()
.authServerUrl(authServerUrl)
.realm(configuration.getRealm())
.clientId(configuration.getClientId())
.credentials(configuration.getResourceCredentials())
.bearerOnly(false)
.enforcerConfig(enforcerConfig)
.httpClient(configuration.getClient()).build();
}
}

View file

@ -1,81 +0,0 @@
package org.keycloak.adapters.authorization.integration.elytron;
import java.io.IOException;
import java.io.InputStream;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.ServiceLoader;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.util.JsonSerialization;
/**
* A {@link ServletContextListener} to programmatically configure the {@link ServletContext} in order to
* enable the policy enforcer.</p>
*
* By default, the policy enforcer configuration is loaded from a file at {@code WEB-INF/policy-enforcer.json}.</p>
*
* Applications can also dynamically resolve the configuration by implementing the {@link ConfigurationResolver} SPI. For that,
* make sure to create a {@link META-INF/services/org.keycloak.adapters.authorization.spi.ConfigurationResolver} to register
* the implementation.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@WebListener
public class PolicyEnforcerServletContextListener implements ServletContextListener {
private final Logger logger = Logger.getLogger(getClass());
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
Iterator<ConfigurationResolver> configResolvers = ServiceLoader.load(ConfigurationResolver.class).iterator();
ConfigurationResolver configResolver;
if (configResolvers.hasNext()) {
configResolver = configResolvers.next();
if (configResolvers.hasNext()) {
throw new IllegalStateException("Multiple " + ConfigurationResolver.class.getName() + " implementations found");
}
logger.debugf("Configuration resolver found from classpath: %s", configResolver);
} else {
String enforcerConfigLocation = "WEB-INF/policy-enforcer.json";
InputStream config = servletContext.getResourceAsStream(enforcerConfigLocation);
if (config == null) {
logger.debugf("Could not find the policy enforcer configuration file: %s", enforcerConfigLocation);
return;
}
try {
configResolver = createDefaultConfigurationResolver(JsonSerialization.readValue(config, PolicyEnforcerConfig.class));
} catch (IOException e) {
throw new RuntimeException("Failed to parse policy enforcer configuration: " + enforcerConfigLocation);
}
}
logger.debug("Policy enforcement filter is enabled.");
servletContext.addFilter("keycloak-policy-enforcer", new ElytronPolicyEnforcerFilter(configResolver))
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
}
private ConfigurationResolver createDefaultConfigurationResolver(PolicyEnforcerConfig enforcerConfig) {
return new ConfigurationResolver() {
@Override
public PolicyEnforcerConfig resolve(HttpRequest request) {
return enforcerConfig;
}
};
}
}

View file

@ -1,139 +0,0 @@
/*
* Copyright 2023 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.integration.elytron;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ServletHttpRequest implements HttpRequest {
private final HttpServletRequest request;
private final TokenPrincipal tokenPrincipal;
private InputStream inputStream;
public ServletHttpRequest(HttpServletRequest request, TokenPrincipal tokenPrincipal) {
this.request = request;
this.tokenPrincipal = tokenPrincipal;
}
@Override
public String getRelativePath() {
String uri = request.getRequestURI();
String contextPath = request.getContextPath();
String servletPath = uri.substring(uri.indexOf(contextPath) + contextPath.length());
if ("".equals(servletPath)) {
servletPath = "/";
}
return servletPath;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public String getURI() {
return request.getRequestURI();
}
@Override
public List<String> getHeaders(String name) {
return Collections.list(request.getHeaders(name));
}
@Override
public String getFirstParam(String name) {
Map<String, String[]> parameters = request.getParameterMap();
String[] values = parameters.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String getCookieValue(String name) {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
return null;
}
@Override
public String getRemoteAddr() {
return request.getRemoteAddr();
}
@Override
public boolean isSecure() {
return request.isSecure();
}
@Override
public String getHeader(String name) {
return request.getHeader(name);
}
@Override
public InputStream getInputStream(boolean buffered) {
if (inputStream != null) {
return inputStream;
}
if (buffered) {
try {
return inputStream = new BufferedInputStream(request.getInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try {
return request.getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public TokenPrincipal getPrincipal() {
return tokenPrincipal;
}
}

View file

@ -1,58 +0,0 @@
/*
* Copyright 2023 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.integration.elytron;
import java.io.IOException;
import jakarta.servlet.http.HttpServletResponse;
import org.keycloak.adapters.authorization.spi.HttpResponse;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ServletHttpResponse implements HttpResponse {
private HttpServletResponse response;
public ServletHttpResponse(HttpServletResponse response) {
this.response = response;
}
@Override
public void sendError(int status) {
try {
response.sendError(status);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void sendError(int status, String reason) {
try {
response.sendError(status, reason);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void setHeader(String name, String value) {
response.setHeader(name, value);
}
}

View file

@ -1,125 +0,0 @@
package org.keycloak.adapters.authorization.integration.jakarta;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContextAttributeListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest;
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse;
import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.wildfly.security.http.oidc.OidcClientConfiguration;
import org.wildfly.security.http.oidc.OidcPrincipal;
import org.wildfly.security.http.oidc.RefreshableOidcSecurityContext;
/**
* A Jakarta Servlet {@link Filter} acting as a policy enforcer. This filter does not enforce access for anonymous subjects.</p>
*
* For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if
* the request should continue.</p>
*
* If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly
* respond to client.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ServletPolicyEnforcerFilter implements Filter, ServletContextAttributeListener {
private final Logger logger = Logger.getLogger(getClass());
private final Map<PolicyEnforcerConfig, PolicyEnforcer> policyEnforcer;
private final ConfigurationResolver configResolver;
public ServletPolicyEnforcerFilter(ConfigurationResolver configResolver) {
this.configResolver = configResolver;
this.policyEnforcer = Collections.synchronizedMap(new HashMap<>());
}
@Override
public void init(FilterConfig filterConfig) {
// no-init
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
ServletHttpRequest httpRequest = new ServletHttpRequest(request, new TokenPrincipal() {
@Override
public String getRawToken() {
return extractBearerToken(request);
}
});
PolicyEnforcer policyEnforcer = getOrCreatePolicyEnforcer(request, httpRequest);
AuthorizationContext authzContext = policyEnforcer.enforce(httpRequest, new ServletHttpResponse(response));
request.setAttribute(AuthorizationContext.class.getName(), authzContext);
if (authzContext.isGranted()) {
logger.debug("Request authorized, continuing the filter chain");
filterChain.doFilter(servletRequest, servletResponse);
} else {
logger.debugf("Unauthorized request to path [%s], aborting the filter chain", request.getRequestURI());
}
}
protected String extractBearerToken(HttpServletRequest request) {
Enumeration<String> authorizationHeaderValues = request.getHeaders("Authorization");
while (authorizationHeaderValues.hasMoreElements()) {
String value = authorizationHeaderValues.nextElement();
String[] parts = value.trim().split("\\s+");
if (parts.length != 2) {
continue;
}
String bearer = parts[0];
if (bearer.equalsIgnoreCase("Bearer")) {
return parts[1];
}
}
return null;
}
private PolicyEnforcer getOrCreatePolicyEnforcer(HttpServletRequest servletRequest, HttpRequest request) {
return policyEnforcer.computeIfAbsent(configResolver.resolve(request), new Function<PolicyEnforcerConfig, PolicyEnforcer>() {
@Override
public PolicyEnforcer apply(PolicyEnforcerConfig enforcerConfig) {
return createPolicyEnforcer(servletRequest, enforcerConfig);
}
});
}
protected PolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, PolicyEnforcerConfig enforcerConfig) {
String authServerUrl = enforcerConfig.getAuthServerUrl();
return PolicyEnforcer.builder()
.authServerUrl(authServerUrl)
.realm(enforcerConfig.getRealm())
.clientId(enforcerConfig.getResource())
.credentials(enforcerConfig.getCredentials())
.bearerOnly(false)
.enforcerConfig(enforcerConfig).build();
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2023 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.spi;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
/**
* Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}.</p>
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ConfigurationResolver {
/**
* Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}.
*
* @param request the request
* @return the policy enforcer configuration for the given request
*/
PolicyEnforcerConfig resolve(HttpRequest request);
}

View file

@ -1,114 +0,0 @@
/*
* Copyright 2023 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.spi;
import java.io.InputStream;
import java.util.List;
import org.keycloak.adapters.authorization.TokenPrincipal;
/**
* Represents an incoming HTTP request and the contract to manipulate it.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface HttpRequest {
/**
* Get the request path. This is the path relative to the context path.
* E.g.: for a HTTP GET request to http://my.appserver.com/my-application/path/sub-path this method is going to return /path/sub-path.
* @return the relative path
*/
String getRelativePath();
/**
* Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
*
* @return a {@code String} specifying the name of the method with which this request was made
*/
String getMethod();
/**
* Get the URI representation for the current request.
*
* @return a {@code String} representation for the current request
*/
String getURI();
/**
* Get a list of all of the values set for the specified header within the HTTP request.
*
* @param name the header name
* @return a list of the values set for this header, if the header is not set on the request then null should be returned
*/
List<String> getHeaders(String name);
/**
* Get the first value for a parameter with the given {@code name}
*
* @param name the parameter name
* @return the value of the parameter
*/
String getFirstParam(String name);
/**
* Get the first value for a cookie with the given {@code name}.
*
* @param name the parameter name
* @return the value of the cookie
*/
String getCookieValue(String name);
/**
* Returns the client address.
*
* @return the client address.
*/
String getRemoteAddr();
/**
* Indicates if the request is coming from a secure channel through HTTPS.
*
* @return {@code true} if the HTTP scheme is set to 'https'. Otherwise, {@code false}
*/
boolean isSecure();
/**
* Get the first value for a HEADER with the given {@code name}.
*
* @param name the HEADER name
* @return the value of the HEADER
*/
String getHeader(String name);
/**
* Returns the request input stream
*
* @param buffered if the input stream should be buffered and support for multiple reads
* @return the request input stream
*/
InputStream getInputStream(boolean buffered);
/**
* Returns a {@link TokenPrincipal} associated with the request.
*
* @return the principal
*/
TokenPrincipal getPrincipal();
}

View file

@ -1,48 +0,0 @@
/*
* Copyright 2023 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.spi;
/**
* Represents an outgoing HTTP response and the contract to manipulate it.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface HttpResponse {
/**
* Send an error with the given {@code statusCode}.
*
* @param statusCode the status to set in the response
*/
void sendError(int statusCode);
/**
* Send an error with the given {@code statusCode} and {@code reason} message.
*
* @param statusCode the status to set in the response
*/
void sendError(int statusCode, String reason);
/**
* Set a header with the given {@code name} and {@code value}.
*
* @param name the header name
* @param value the header value
*/
void setHeader(String name, String value);
}

View file

@ -1,78 +0,0 @@
/*
* Copyright 2018 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.util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.JsonSerialization;
/**
* Utility methods to manipulate JSON data
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class JsonUtils {
public static List<String> getValues(JsonNode jsonNode, String path) {
return getValues(jsonNode.at(path));
}
public static List<String> getValues(JsonNode jsonNode) {
List<String> values = new ArrayList<>();
if (jsonNode.isArray()) {
for (JsonNode node : jsonNode) {
String value;
if (node.isObject()) {
try {
value = JsonSerialization.writeValueAsString(node);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
value = node.asText();
}
if (value != null) {
values.add(value);
}
}
} else {
String value = jsonNode.asText();
if (value != null) {
values.add(value);
}
}
return values;
}
public static AccessToken asAccessToken(String rawToken) {
try {
return new JWSInput(rawToken).readJsonContent(AccessToken.class);
} catch (Exception cause) {
throw new RuntimeException("Failed to decode token", cause);
}
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright 2018 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.util;
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderResolver {
public static final String NAME = "keycloak";
@Override
public List<String> resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
TokenPrincipal principal = request.getPrincipal();
if (source.endsWith("access_token")) {
return Arrays.asList(principal.getRawToken());
}
JsonNode jsonNode;
if (source.startsWith("access_token[")) {
jsonNode = JsonSerialization.mapper.valueToTree(principal.getToken());
} else {
throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
}
return JsonUtils.getValues(jsonNode, getParameter(source, "Invalid placeholder [" + placeHolder + "]"));
}
}

View file

@ -1,30 +0,0 @@
/*
* Copyright 2018 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.util;
import java.util.List;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface PlaceHolderResolver {
List<String> resolve(String placeHolder, HttpRequest httpFacade);
}

View file

@ -1,112 +0,0 @@
/*
* Copyright 2018 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.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PlaceHolders {
private static Map<String, PlaceHolderResolver> resolvers = new HashMap<>();
static {
resolvers.put(RequestPlaceHolderResolver.NAME, new RequestPlaceHolderResolver());
resolvers.put(KeycloakSecurityContextPlaceHolderResolver.NAME, new KeycloakSecurityContextPlaceHolderResolver());
}
private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
public static List<String> resolve(String value, HttpRequest httpFacade) {
Map<String, List<String>> placeHolders = parsePlaceHolders(value, httpFacade);
if (!placeHolders.isEmpty()) {
value = formatPlaceHolder(value);
for (Entry<String, List<String>> entry : placeHolders.entrySet()) {
List<String> values = entry.getValue();
if (values.isEmpty() || values.size() > 1) {
return values;
}
value = value.replaceAll(entry.getKey(), values.get(0)).trim();
}
}
return Arrays.asList(value);
}
static String getParameter(String source, String messageIfNotFound) {
Matcher matcher = PLACEHOLDER_PARAM_PATTERN.matcher(source);
while (matcher.find()) {
return matcher.group(1).replaceAll("'", "");
}
if (messageIfNotFound != null) {
throw new RuntimeException(messageIfNotFound);
}
return null;
}
private static Map<String, List<String>> parsePlaceHolders(String value, HttpRequest httpFacade) {
Map<String, List<String>> placeHolders = Collections.emptyMap();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
boolean found = matcher.find();
if (found) {
placeHolders = new HashMap<>();
do {
String placeHolder = matcher.group(1);
int resolverNameIdx = placeHolder.indexOf('.');
if (resolverNameIdx == -1) {
throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
}
PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
if (resolver != null) {
List<String> resolved = resolver.resolve(placeHolder, httpFacade);
if (resolved != null) {
placeHolders.put(formatPlaceHolder(placeHolder), resolved);
}
}
} while (matcher.find());
}
return placeHolders;
}
private static String formatPlaceHolder(String placeHolder) {
return placeHolder.replaceAll("\\{", "").replace("}", "").replace("[", "").replace("]", "").replace("[", "").replace("]", "");
}
}

View file

@ -1,170 +0,0 @@
/*
* Copyright 2018 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.util;
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RequestPlaceHolderResolver implements PlaceHolderResolver {
static String NAME = "request";
@Override
public List<String> resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
if (source.startsWith("parameter")) {
String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]");
String parameterValue = request.getFirstParam(parameterName);
if (parameterValue != null) {
return Arrays.asList(parameterValue);
}
} else if (source.startsWith("header")) {
String headerName = getParameter(source, "Could not obtain header name from placeholder [" + source + "]");
List<String> headerValue = request.getHeaders(headerName);
if (headerValue != null) {
return headerValue;
}
} else if (source.startsWith("cookie")) {
String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
String cookieValue = request.getCookieValue(cookieName);
if (cookieValue != null) {
return Arrays.asList(cookieValue);
}
} else if (source.startsWith("remoteAddr")) {
String value = request.getRemoteAddr();
if (value != null) {
return Arrays.asList(value);
}
} else if (source.startsWith("method")) {
String value = request.getMethod();
if (value != null) {
return Arrays.asList(value);
}
} else if (source.startsWith("uri")) {
String value = request.getURI();
if (value != null) {
return Arrays.asList(value);
}
} else if (source.startsWith("relativePath")) {
String value = request.getRelativePath();
if (value != null) {
return Arrays.asList(value);
}
} else if (source.startsWith("secure")) {
return Arrays.asList(String.valueOf(request.isSecure()));
} else if (source.startsWith("body")) {
String contentType = request.getHeader("Content-Type");
if (contentType == null) {
contentType = "";
} else if (contentType.indexOf(';') != -1){
contentType = contentType.substring(0, contentType.indexOf(';')).trim();
}
InputStream body = request.getInputStream(true);
try {
if (body == null || body.available() == 0) {
return Collections.emptyList();
}
} catch (IOException cause) {
throw new RuntimeException("Failed to check available bytes in request input stream", cause);
}
if (body.markSupported()) {
body.mark(0);
}
List<String> values = new ArrayList<>();
try {
switch (contentType) {
case "application/json":
try {
JsonNode jsonNode = JsonSerialization.mapper.readTree(new BufferedInputStream(body) {
@Override
public void close() {
// we can't close the stream because it may be used later by the application
}
});
String path = getParameter(source, null);
if (path == null) {
values.addAll(JsonUtils.getValues(jsonNode));
} else {
values.addAll(JsonUtils.getValues(jsonNode, path));
}
} catch (IOException cause) {
throw new RuntimeException("Could not extract claim from request JSON body", cause);
}
break;
default:
StringBuilder value = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(body));
try {
int ch;
while ((ch = reader.read()) != -1) {
value.append((char) ch);
}
} catch (IOException cause) {
throw new RuntimeException("Could not extract claim from request body", cause);
}
values.add(value.toString());
}
} finally {
if (body.markSupported()) {
try {
body.reset();
} catch (IOException cause) {
throw new RuntimeException("Failed to reset request input stream", cause);
}
}
}
return values;
}
return Collections.emptyList();
}
}

View file

@ -1,19 +0,0 @@
#
# * Copyright 2018 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.
#
org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory
org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory

View file

@ -20,6 +20,5 @@
<modules> <modules>
<module>policy</module> <module>policy</module>
<module>client</module> <module>client</module>
<module>policy-enforcer</module>
</modules> </modules>
</project> </project>

View file

@ -1052,11 +1052,6 @@
<artifactId>keycloak-authz-policy-common</artifactId> <artifactId>keycloak-authz-policy-common</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer-tests</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Quarkus --> <!-- Quarkus -->
<dependency> <dependency>

View file

@ -1,219 +0,0 @@
/*
* Copyright 2024 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.testsuite.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.spi.HttpResponse;
/**
*
* @author rmartinc
*/
public class AuthzTestUtils {
private AuthzTestUtils() {
}
public static InputStream httpsAwareConfigurationStream(InputStream input) throws IOException {
if (!ServerURLs.AUTH_SERVER_SSL_REQUIRED) {
return input;
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try (PrintWriter pw = new PrintWriter(out);
Scanner s = new Scanner(input)) {
while (s.hasNextLine()) {
String lineWithReplaces = s.nextLine().replace("http://localhost:8180/auth",
ServerURLs.AUTH_SERVER_SCHEME + "://localhost:" + ServerURLs.AUTH_SERVER_PORT + "/auth");
pw.println(lineWithReplaces);
}
}
return new ByteArrayInputStream(out.toByteArray());
}
public static InputStream getAdapterConfiguration(String fileName) {
try {
return httpsAwareConfigurationStream(AuthzTestUtils.class.getResourceAsStream("/authorization-test/" + fileName));
} catch (IOException e) {
throw new AssertionError("Could not load keycloak configuration", e);
}
}
public static PolicyEnforcer createPolicyEnforcer(String resource, boolean bearerOnly) {
try (InputStream is = getAdapterConfiguration(resource)) {
return PolicyEnforcer.builder().enforcerConfig(is).bearerOnly(bearerOnly).build();
} catch (IOException e) {
throw new IllegalArgumentException("Invalid resource " + resource, e);
}
}
public static HttpRequest createHttpRequest(String path) {
return createHttpRequest(path, null, null, null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), null);
}
public static HttpRequest createHttpRequest(String path, String token) {
return createHttpRequest(path, null, null, token, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), null);
}
public static HttpRequest createHttpRequest(String path, String token, String method) {
return createHttpRequest(path, null, method, token, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), null);
}
public static HttpRequest createHttpRequest(String path, String token, Map<String, List<String>> parameters) {
return createHttpRequest(path, null, null, token, Collections.emptyMap(), parameters, Collections.emptyMap(), null);
}
public static HttpRequest createHttpRequest(String path, String method, String token, Map<String,
List<String>> headers, Map<String, List<String>> parameters, InputStream requestBody) {
return createHttpRequest(path, null, method, token, headers, parameters, Collections.emptyMap(), null);
}
public static HttpRequest createHttpRequest(String path, String relativePath, String method, String token, Map<String,
List<String>> headers, Map<String, List<String>> parameters, Map<String, String> cookies, InputStream requestBody) {
return new HttpRequest() {
private InputStream inputStream;
@Override
public String getRelativePath() {
return relativePath != null? relativePath : path;
}
@Override
public String getMethod() {
return method == null ? "GET" : method;
}
@Override
public String getURI() {
return path;
}
@Override
public List<String> getHeaders(String name) {
return headers.getOrDefault(name, Collections.emptyList());
}
@Override
public String getFirstParam(String name) {
List<String> values = parameters.getOrDefault(name, Collections.emptyList());
return values.isEmpty()? null : values.iterator().next();
}
@Override
public String getCookieValue(String name) {
return cookies.get(name);
}
@Override
public String getRemoteAddr() {
return "user-remote-addr";
}
@Override
public boolean isSecure() {
return true;
}
@Override
public String getHeader(String name) {
List<String> headers = getHeaders(name);
return headers.isEmpty()? null : headers.iterator().next();
}
@Override
public InputStream getInputStream(boolean buffered) {
if (requestBody == null) {
return new ByteArrayInputStream(new byte[] {});
}
if (inputStream != null) {
return inputStream;
}
if (buffered) {
return inputStream = new BufferedInputStream(requestBody);
}
return requestBody;
}
@Override
public TokenPrincipal getPrincipal() {
return () -> token;
}
};
}
public static class TestResponse implements HttpResponse {
private final Map<String, List<String>> headers;
private int status;
public TestResponse() {
this.headers = new HashMap<>();
}
public TestResponse(Map<String, List<String>> headers) {
this.headers = headers;
}
public int getStatus() {
return status;
}
@Override
public void setHeader(String name, String value) {
headers.put(name, Arrays.asList(value));
}
public Map<String, List<String>> getHeaders() {
return headers;
}
@Override
public void sendError(int code) {
status = code;
}
@Override
public void sendError(int code, String message) {
status = code;
}
public TestResponse clear() {
this.status = -1;
this.headers.clear();
return this;
}
}
}

View file

@ -23,14 +23,15 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
@ -38,6 +39,7 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
@ -60,6 +62,7 @@ import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RolesBuilder; import org.keycloak.testsuite.util.RolesBuilder;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.JsonSerialization;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -346,9 +349,12 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
} }
private AuthzClient getAuthzClient(String adapterConfig) { private AuthzClient getAuthzClient(String adapterConfig) {
PolicyEnforcer policyEnforcer = PolicyEnforcer.builder().enforcerConfig(getConfigurationStream(adapterConfig)).build(); try {
Configuration authzClientConfig = JsonSerialization.readValue(getConfigurationStream(adapterConfig), Configuration.class);
return policyEnforcer.getAuthzClient(); return AuthzClient.create(authzClientConfig);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
} }
private InputStream getConfigurationStream(String adapterConfig) { private InputStream getConfigurationStream(String adapterConfig) {

View file

@ -1,317 +0,0 @@
/*
* Copyright 2018 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.testsuite.authz.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.undertow.Undertow;
import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormDataParser;
import io.undertow.server.handlers.form.FormParserFactory;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.util.AuthzTestUtils;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
private static Undertow httpService;
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
}
@BeforeClass
public static void onBeforeClass() {
httpService = Undertow.builder().addHttpListener(8989, "localhost").setHandler(exchange -> {
if (exchange.isInIoThread()) {
try {
if (exchange.getRelativePath().equals("/post-claim-information-provider")) {
FormParserFactory parserFactory = FormParserFactory.builder().build();
FormDataParser parser = parserFactory.createParser(exchange);
FormData formData = parser.parseBlocking();
if (!("Bearer " + accessTokenString()).equals(exchange.getRequestHeaders().getFirst("Authorization"))
|| !"post".equalsIgnoreCase(exchange.getRequestMethod().toString())
|| !"application/x-www-form-urlencoded".equals(exchange.getRequestHeaders().getFirst("Content-Type"))
|| !exchange.getRequestHeaders().get("header-b").contains("header-b-value1")
|| !exchange.getRequestHeaders().get("header-b").contains("header-b-value2")
|| !formData.get("param-a").getFirst().getValue().equals("param-a-value1")
|| !formData.get("param-a").getLast().getValue().equals("param-a-value2")
|| !formData.get("param-subject").getFirst().getValue().equals("sub")
|| !formData.get("param-user-name").getFirst().getValue().equals("username")
|| !formData.get("param-other-claims").getFirst().getValue().equals("param-other-claims-value1")
|| !formData.get("param-other-claims").getLast().getValue().equals("param-other-claims-value2")) {
exchange.setStatusCode(400);
return;
}
exchange.setStatusCode(200);
} else if (exchange.getRelativePath().equals("/get-claim-information-provider")) {
if (!("Bearer " + accessTokenString()).equals(exchange.getRequestHeaders().getFirst("Authorization"))
|| !"get".equalsIgnoreCase(exchange.getRequestMethod().toString())
|| !exchange.getRequestHeaders().get("header-b").contains("header-b-value1")
|| !exchange.getRequestHeaders().get("header-b").contains("header-b-value2")
|| !exchange.getQueryParameters().get("param-a").contains("param-a-value1")
|| !exchange.getQueryParameters().get("param-a").contains("param-a-value2")
|| !exchange.getQueryParameters().get("param-subject").contains("sub")
|| !exchange.getQueryParameters().get("param-user-name").contains("username")) {
exchange.setStatusCode(400);
return;
}
exchange.setStatusCode(200);
} else {
exchange.setStatusCode(404);
}
} finally {
if (exchange.getStatusCode() == 200) {
try {
ObjectMapper mapper = JsonSerialization.mapper;
JsonParser jsonParser = mapper.getFactory().createParser("{\"a\": \"a-value1\", \"b\": \"b-value1\", \"d\": [\"d-value1\", \"d-value2\"]}");
TreeNode treeNode = mapper.readTree(jsonParser);
exchange.getResponseSender().send(treeNode.toString());
} catch (Exception ignore) {
ignore.printStackTrace();
}
}
exchange.endExchange();
}
}
}).build();
httpService.start();
}
@AfterClass
public static void onAfterClass() {
if (httpService != null) {
httpService.stop();
}
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadRealm(getClass().getResourceAsStream("/authorization-test/test-authz-realm.json"));
testRealms.add(realm);
}
private ClaimInformationPointProvider getClaimInformationProviderForPath(String path, String providerName) {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-config-claims-provider.json", true);
Map<String, ClaimInformationPointProviderFactory> providers = policyEnforcer.getClaimInformationPointProviderFactories();
PathConfig pathConfig = policyEnforcer.getPaths().get(path);
assertNotNull(pathConfig);
Map<String, Map<String, Object>> cipConfig = pathConfig.getClaimInformationPointConfig();
assertNotNull(cipConfig);
ClaimInformationPointProviderFactory factory = providers.get(providerName);
assertNotNull(factory);
Map<String, Object> claimsConfig = cipConfig.get(providerName);
return factory.create(claimsConfig);
}
@Test
public void testBasicClaimsInformationPoint() {
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims")
.resolve(createHttpRequest());
assertEquals("parameter-a", claims.get("claim-from-request-parameter").get(0));
assertEquals("header-b", claims.get("claim-from-header").get(0));
assertEquals("cookie-c", claims.get("claim-from-cookie").get(0));
assertEquals("user-remote-addr", claims.get("claim-from-remoteAddr").get(0));
assertEquals("GET", claims.get("claim-from-method").get(0));
assertEquals("/app/request-uri", claims.get("claim-from-uri").get(0));
assertEquals("/request-relative-path", claims.get("claim-from-relativePath").get(0));
assertEquals("true", claims.get("claim-from-secure").get(0));
assertEquals("static value", claims.get("claim-from-static-value").get(0));
assertEquals("static", claims.get("claim-from-multiple-static-value").get(0));
assertEquals("value", claims.get("claim-from-multiple-static-value").get(1));
assertEquals("Test param-other-claims-value1 and parameter-a", claims.get("param-replace-multiple-placeholder").get(0));
}
@Test
public void testBodyJsonClaimsInformationPoint() throws Exception {
Map<String, List<String>> headers = new HashMap<>();
headers.put("Content-Type", Arrays.asList("application/json"));
ObjectMapper mapper = JsonSerialization.mapper;
JsonParser parser = mapper.getFactory().createParser("{\"a\": {\"b\": {\"c\": \"c-value\"}}, \"d\": [\"d-value1\", \"d-value2\"], \"e\": {\"number\": 123}}");
TreeNode treeNode = mapper.readTree(parser);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(
createHttpRequest(headers, new ByteArrayInputStream(treeNode.toString().getBytes())));
assertEquals("c-value", claims.get("claim-from-json-body-object").get(0));
assertEquals("d-value2", claims.get("claim-from-json-body-array").get(0));
assertEquals("123", claims.get("claim-from-json-body-number").get(0));
}
@Test
public void testBodyJsonObjectClaim() throws Exception {
Map<String, List<String>> headers = new HashMap<>();
headers.put("Content-Type", Arrays.asList("application/json"));
ObjectMapper mapper = JsonSerialization.mapper;
JsonParser parser = mapper.getFactory().createParser("{\"Individual\" : {\n"
+ "\n"
+ " \"Name\": \"John\",\n"
+ "\n"
+ " \"Lastname\": \"Doe\",\n"
+ "\n"
+ " \"individualRoles\" : [ {\n"
+ "\n"
+ " \"roleSpec\": 2342,\n"
+ "\n"
+ " \"roleId\": 4234},\n"
+ "\n"
+ "{\n"
+ "\n"
+ " \"roleSpec\": 4223,\n"
+ "\n"
+ " \"roleId\": 523\n"
+ "\n"
+ " }\n"
+ "\n"
+ " ]\n"
+ "\n"
+ "}}");
TreeNode treeNode = mapper.readTree(parser);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims")
.resolve(createHttpRequest(headers, new ByteArrayInputStream(treeNode.toString().getBytes())));
assertEquals(1, claims.size());
assertEquals(2, claims.get("individualRoles").size());
assertEquals("{\"roleSpec\":2342,\"roleId\":4234}", claims.get("individualRoles").get(0));
assertEquals("{\"roleSpec\":4223,\"roleId\":523}", claims.get("individualRoles").get(1));
headers.put("Content-Type", Arrays.asList("application/json; charset=utf-8"));
claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims")
.resolve(createHttpRequest(headers, new ByteArrayInputStream(treeNode.toString().getBytes())));
assertEquals(1, claims.size());
assertEquals(2, claims.get("individualRoles").size());
assertEquals("{\"roleSpec\":2342,\"roleId\":4234}", claims.get("individualRoles").get(0));
assertEquals("{\"roleSpec\":4223,\"roleId\":523}", claims.get("individualRoles").get(1));
}
@Test
public void testBodyClaimsInformationPoint() {
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims")
.resolve(createHttpRequest(new HashMap<>(), new ByteArrayInputStream("raw-body-text".getBytes())));
assertEquals("raw-body-text", claims.get("claim-from-body").get(0));
}
@Test
public void testHttpClaimInformationPointProviderWithoutClaims() {
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-get-claim-provider", "http")
.resolve(createHttpRequest(new HashMap<>(), null));
assertEquals("a-value1", claims.get("a").get(0));
assertEquals("b-value1", claims.get("b").get(0));
assertEquals("d-value1", claims.get("d").get(0));
assertEquals("d-value2", claims.get("d").get(1));
assertNull(claims.get("claim-a"));
assertNull(claims.get("claim-d"));
assertNull(claims.get("claim-d0"));
assertNull(claims.get("claim-d-all"));
}
@Test
public void testHttpClaimInformationPointProviderWithClaims() {
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-post-claim-provider", "http")
.resolve(createHttpRequest(new HashMap<>(), null));
assertEquals("a-value1", claims.get("claim-a").get(0));
assertEquals("d-value1", claims.get("claim-d").get(0));
assertEquals("d-value2", claims.get("claim-d").get(1));
assertEquals("d-value1", claims.get("claim-d0").get(0));
assertEquals("d-value1", claims.get("claim-d-all").get(0));
assertEquals("d-value2", claims.get("claim-d-all").get(1));
assertNull(claims.get("a"));
assertNull(claims.get("b"));
assertNull(claims.get("d"));
}
private static HttpRequest createHttpRequest() {
return createHttpRequest(new HashMap<>(), null);
}
private static HttpRequest createHttpRequest(Map<String, List<String>> headers, InputStream requestBody) {
Map<String, List<String>> queryParameter = new HashMap<>();
queryParameter.put("a", Arrays.asList("parameter-a"));
headers.put("b", Arrays.asList("header-b"));
Map<String, String> cookies = new HashMap<>();
cookies.put("c", "cookie-c");
return AuthzTestUtils.createHttpRequest("/app/request-uri", "/request-relative-path", "GET",
accessTokenString(), headers, queryParameter, cookies, requestBody);
}
private static AccessToken accessToken() {
AccessToken token = new AccessToken();
token.subject("sub");
token.setPreferredUsername("username");
token.getOtherClaims().put("custom_claim", Arrays.asList("param-other-claims-value1", "param-other-claims-value2"));
return token;
}
private static String accessTokenString() {
return new JWSBuilder().jsonContent(accessToken()).none();
}
}

View file

@ -1,84 +0,0 @@
/*
* 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.testsuite.authz.admin;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AuthzTestUtils;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.keycloak.testsuite.ProfileAssume;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class EnforcerConfigTest extends AbstractKeycloakTest {
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadRealm(getClass().getResourceAsStream("/authorization-test/test-authz-realm.json"));
testRealms.add(realm);
}
@Test
public void testMultiplePathsWithSameName() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-config-paths-same-name.json", true);
Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
assertEquals(1, paths.size());
assertEquals(4, paths.values().iterator().next().getMethods().size());
}
@Test
public void testPathConfigClaimInformationPoint() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-config-path-cip.json", true);
Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
assertEquals(1, paths.size());
PathConfig pathConfig = paths.values().iterator().next();
Map<String, Map<String, Object>> cipConfig = pathConfig.getClaimInformationPointConfig();
assertEquals(1, cipConfig.size());
Map<String, Object> claims = cipConfig.get("claims");
assertNotNull(claims);
assertEquals(3, claims.size());
assertEquals("{request.parameter['a']}", claims.get("claim-a"));
assertEquals("{request.header['b']}", claims.get("claim-b"));
assertEquals("{request.cookie['c']}", claims.get("claim-c"));
}
}

View file

@ -1,57 +0,0 @@
/*
* Copyright 2018 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.testsuite.authz.admin;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.spi.HttpRequest;
public class MyCustomCIPFactory implements ClaimInformationPointProviderFactory<MyCustomCIP> {
@Override
public String getName() {
return "my-custom-cip";
}
@Override
public MyCustomCIP create(Map<String, Object> config) {
return new MyCustomCIP(config);
}
}
class MyCustomCIP implements ClaimInformationPointProvider {
private final Map<String, Object> config;
MyCustomCIP(Map<String, Object> config) {
this.config = config;
}
@Override
public Map<String, List<String>> resolve(HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
claims.put("resolved-claim", Arrays.asList(config.get("claim-value").toString()));
return claims;
}
}

View file

@ -1,386 +0,0 @@
/*
* Copyright 2018 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.testsuite.authz.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.AuthorizationContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.util.AuthzTestUtils;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
import org.keycloak.testsuite.util.RolesBuilder;
import org.keycloak.testsuite.util.UserBuilder;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
protected static final String REALM_NAME = "authz-test";
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(RealmBuilder.create().name(REALM_NAME)
.roles(RolesBuilder.create()
.realmRole(RoleBuilder.create().name("uma_authorization").build())
.realmRole(RoleBuilder.create().name("uma_protection").build())
)
.user(UserBuilder.create().username("marta").password("password")
.addRoles("uma_authorization", "uma_protection")
.role("resource-server-test", "uma_protection"))
.user(UserBuilder.create().username("kolo").password("password"))
.client(ClientBuilder.create().clientId("resource-server-uma-test")
.secret("secret")
.authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-uma-test")
.defaultRoles("uma_protection")
.directAccessGrants())
.client(ClientBuilder.create().clientId("resource-server-test")
.secret("secret")
.authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-test")
.defaultRoles("uma_protection")
.directAccessGrants())
.client(ClientBuilder.create().clientId("public-client-test")
.publicClient()
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*")
.directAccessGrants())
.build());
}
@Test
public void testEnforceUMAAccessWithClaimsUsingBearerToken() {
initAuthorizationSettings(getClientResource("resource-server-uma-test"));
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-uma-claims-test.json", true);
HashMap<String, List<String>> headers = new HashMap<>();
HashMap<String, List<String>> parameters = new HashMap<>();
parameters.put("withdrawal.amount", Arrays.asList("50"));
AuthzClient authzClient = policyEnforcer.getAuthzClient();
String token = authzClient.obtainAccessToken("marta", "password").getToken();
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse);
assertFalse(context.isGranted());
AuthorizationRequest request = new AuthorizationRequest();
request.setTicket(extractTicket(testResponse.getHeaders()));
AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(request);
token = response.getToken();
assertNotNull(token);
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse.clear());
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse.clear());
request = new AuthorizationRequest();
request.setTicket(extractTicket(testResponse.getHeaders()));
response = authzClient.authorization("marta", "password").authorize(request);
token = response.getToken();
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "POST", token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", "GET", token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
assertEquals(1, context.getPermissions().size());
Permission permission = context.getPermissions().get(0);
assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next());
}
@Test
public void testEnforceEntitlementAccessWithClaimsWithoutBearerToken() {
initAuthorizationSettings(getClientResource("resource-server-test"));
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-entitlement-claims-test.json", false);
HashMap<String, List<String>> headers = new HashMap<>();
HashMap<String, List<String>> parameters = new HashMap<>();
AuthzClient authzClient = policyEnforcer.getAuthzClient();
String token = authzClient.obtainAccessToken("marta", "password").getToken();
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse);
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
assertEquals(1, context.getPermissions().size());
Permission permission = context.getPermissions().get(0);
assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
assertEquals(1, context.getPermissions().size());
permission = context.getPermissions().get(0);
assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next());
}
@Test
public void testEnforceEntitlementAccessWithClaimsWithBearerToken() {
initAuthorizationSettings(getClientResource("resource-server-test"));
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-entitlement-claims-test.json", false);
HashMap<String, List<String>> headers = new HashMap<>();
HashMap<String, List<String>> parameters = new HashMap<>();
AuthzClient authzClient = policyEnforcer.getAuthzClient();
String token = authzClient.obtainAccessToken("marta", "password").getToken();
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse);
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
}
@Test
public void testEnforceEntitlementAccessWithClaimsWithBearerTokenFromPublicClient() {
initAuthorizationSettings(getClientResource("resource-server-test"));
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-entitlement-claims-test.json", false);
HashMap<String, List<String>> headers = new HashMap<>();
HashMap<String, List<String>> parameters = new HashMap<>();
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse);
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/bank/account/1/withdrawal", null, token, headers, parameters, null),
testResponse.clear());
assertTrue(context.isGranted());
}
private String extractTicket(Map<String, List<String>> headers) {
List<String> wwwAuthenticateHeader = headers.get("WWW-Authenticate");
assertNotNull(wwwAuthenticateHeader);
assertFalse(wwwAuthenticateHeader.isEmpty());
String wwwAuthenticate = wwwAuthenticateHeader.get(0);
return wwwAuthenticate.substring(wwwAuthenticate.indexOf("ticket=") + "ticket=\"".length(), wwwAuthenticate.lastIndexOf('"'));
}
private void initAuthorizationSettings(ClientResource clientResource) {
if (clientResource.authorization().resources().findByName("Bank Account").isEmpty()) {
JSPolicyRepresentation policy = new JSPolicyRepresentation();
policy.setName("Withdrawal Limit Policy");
policy.setType("script-scripts/enforce-withdraw-limit-policy.js");
clientResource.authorization().policies().js().create(policy).close();
createResource(clientResource, "Bank Account", "/api/bank/account/{id}/withdrawal", "withdrawal");
ScopePermissionRepresentation permission = new ScopePermissionRepresentation();
permission.setName("Withdrawal Permission");
permission.addScope("withdrawal");
permission.addPolicy(policy.getName());
clientResource.authorization().permissions().scope().create(permission).close();
}
}
private ResourceRepresentation createResource(ClientResource clientResource, String name, String uri, String... scopes) {
ResourceRepresentation representation = new ResourceRepresentation();
representation.setName(name);
representation.setUri(uri);
representation.setScopes(Arrays.asList(scopes).stream().map(ScopeRepresentation::new).collect(Collectors.toSet()));
try (jakarta.ws.rs.core.Response response = clientResource.authorization().resources().create(representation)) {
representation.setId(response.readEntity(ResourceRepresentation.class).getId());
return representation;
}
}
private ClientResource getClientResource(String name) {
ClientsResource clients = realmsResouce().realm(REALM_NAME).clients();
ClientRepresentation representation = clients.findByClientId(name).get(0);
return clients.get(representation.getId());
}
}

View file

@ -1,706 +0,0 @@
/*
* Copyright 2018 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.testsuite.authz.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.AuthorizationContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.PermissionsResource;
import org.keycloak.admin.client.resource.ResourcesResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.util.AuthzTestUtils;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
import org.keycloak.testsuite.util.RolesBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcerTest extends AbstractKeycloakTest {
private static final String RESOURCE_SERVER_CLIENT_ID = "resource-server-test";
private static final String REALM_NAME = "authz-test";
@BeforeClass
public static void enabled() {
ProfileAssume.assumeFeatureEnabled(AUTHORIZATION);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(RealmBuilder.create().name(REALM_NAME)
.roles(RolesBuilder.create()
.realmRole(RoleBuilder.create().name("uma_authorization").build())
.realmRole(RoleBuilder.create().name("uma_protection").build())
.realmRole(RoleBuilder.create().name("user").build())
)
.user(UserBuilder.create().username("marta").password("password")
.addRoles("uma_authorization", "uma_protection", "user")
.role("resource-server-test", "uma_protection"))
.user(UserBuilder.create().username("kolo").password("password"))
.client(ClientBuilder.create().clientId("resource-server-uma-test")
.secret("secret")
.authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-uma-test")
.defaultRoles("uma_protection")
.directAccessGrants())
.client(ClientBuilder.create().clientId("resource-server-test")
.secret("secret")
.authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-test")
.defaultRoles("uma_protection")
.directAccessGrants())
.client(ClientBuilder.create().clientId("public-client-test")
.publicClient()
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*")
.directAccessGrants())
.build());
}
@Before
public void onBefore() {
initAuthorizationSettings(getClientResource(RESOURCE_SERVER_CLIENT_ID));
}
@Test
public void testBearerOnlyClientResponse() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only.json", true);
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea"), testResponse);
assertFalse(context.isGranted());
assertEquals(403, testResponse.getStatus());
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea", token), testResponse.clear());
assertTrue(context.isGranted());
testResponse = new AuthzTestUtils.TestResponse();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourceb"), testResponse.clear());
assertFalse(context.isGranted());
assertEquals(403, testResponse.getStatus());
}
@Test
public void testPathConfigurationPrecendenceWhenLazyLoadingPaths() throws IOException {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-paths.json", false);
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea"), testResponse);
assertFalse(context.isGranted());
assertEquals(403, testResponse.getStatus());
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea", token), testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/"), testResponse.clear());
assertTrue(context.isGranted());
}
@Test
public void testResolvingClaimsOnce() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only-with-cip.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(
AuthzTestUtils.createHttpRequest("/api/resourcea", token, Collections.singletonMap("claim-a", Collections.singletonList("value-claim-a"))),
new AuthzTestUtils.TestResponse());
Permission permission = context.getPermissions().get(0);
Map<String, Set<String>> claims = permission.getClaims();
assertTrue(context.isGranted());
assertEquals("value-claim-a", claims.get("claim-a").iterator().next());
assertEquals("claim-b", claims.get("claim-b").iterator().next());
}
@Test
public void testCustomClaimProvider() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only-with-cip.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea", token), new AuthzTestUtils.TestResponse());
Permission permission = context.getPermissions().get(0);
Map<String, Set<String>> claims = permission.getClaims();
assertTrue(context.isGranted());
assertEquals("test", claims.get("resolved-claim").iterator().next());
}
@Test
public void testOnDenyRedirectTo() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-on-deny-redirect.json", false);
AuthzTestUtils.TestResponse response = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea"), response);
assertFalse(context.isGranted());
assertEquals(302, response.getStatus());
List<String> location = response.getHeaders().getOrDefault("Location", Collections.emptyList());
assertFalse(location.isEmpty());
assertEquals("/accessDenied", location.get(0));
}
@Test
public void testNotAuthenticatedDenyUnmapedPath() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only.json", true);
AuthzTestUtils.TestResponse response = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/unmmaped"), response);
assertFalse(context.isGranted());
assertEquals(403, response.getStatus());
}
@Test
public void testMappedPathEnforcementModeDisabled() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-disabled-enforce-mode-path.json", true);
AuthzTestUtils.TestResponse response = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/public"), response);
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourceb"), response.clear());
assertFalse(context.isGranted());
assertEquals(403, response.getStatus());
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), null).getAccessToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourcea", token), response.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resourceb", token), response.clear());
assertFalse(context.isGranted());
assertEquals(403, response.getStatus());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/public", token), response.clear());
assertTrue(context.isGranted());
}
@Test
public void testDisabledPathNoCache() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-disabled-path-nocache.json", true);
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/public"), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
ResourceRepresentation resource = clientResource.authorization().resources()
.findByName("Root").get(0);
clientResource.authorization().resources().resource(resource.getId()).remove();
// first request caches the path and the entry is invalidated due to the lifespan
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/all-public"), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
WaitUtils.pause(1000);
// second request can not fail because entry should not be invalidated
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/all-public"), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
}
@Test
public void testLazyLoadedPathIsCached() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
createResource(clientResource, "Static Test Resource", "/api/any-resource/*");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName("Any Resource Permission");
permission.addResource("Static Test Resource");
permission.addPolicy("Always Grant Policy");
clientResource.authorization().permissions().resource().create(permission);
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-disabled-path-nocache.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/any-resource/test", token), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/any-resource/test", token), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
ResourceRepresentation resource = clientResource.authorization().resources()
.findByName("Static Test Resource").get(0);
clientResource.authorization().resources().resource(resource.getId()).remove();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/any-resource/test", token), new AuthzTestUtils.TestResponse());
assertFalse(context.isGranted());
}
@Test
public void testEnforcementModeDisabled() {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-disabled-enforce-mode.json", true);
AuthzTestUtils.TestResponse response = new AuthzTestUtils.TestResponse();
policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource/public"), response);
assertEquals(401, response.getStatus());
}
@Test
public void testMatchHttpVerbsToScopes() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
ResourceRepresentation resource = createResource(clientResource, "Resource With HTTP Scopes", "/api/resource-with-scope");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy("Always Grant Policy");
PermissionsResource permissions = clientResource.authorization().permissions();
permissions.resource().create(permission).close();
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-match-http-verbs-scopes.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse);
assertFalse("Should fail because resource does not have any scope named GET", context.isGranted());
assertEquals(403, testResponse.getStatus());
resource.addScope("GET", "POST");
clientResource.authorization().resources().resource(resource.getId()).update(resource);
policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-match-http-verbs-scopes.json", true);
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "POST"), testResponse.clear());
assertTrue(context.isGranted());
// create a PATCH scope without associated it with the resource so that a PATCH request is denied accordingly even though
// the scope exists on the server
clientResource.authorization().scopes().create(new ScopeRepresentation("PATCH"));
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "PATCH"), testResponse.clear());
assertFalse(context.isGranted());
ScopePermissionRepresentation postPermission = new ScopePermissionRepresentation();
postPermission.setName("GET permission");
postPermission.addScope("GET");
postPermission.addPolicy("Always Deny Policy");
permissions.scope().create(postPermission).close();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertFalse(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
postPermission.addScope("GET");
postPermission.addPolicy("Always Grant Policy");
permissions.scope().findById(postPermission.getId()).update(postPermission);
AuthzClient authzClient = policyEnforcer.getAuthzClient();
AuthorizationResponse authorize = authzClient.authorization(token).authorize();
token = authorize.getToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "POST"), testResponse.clear());
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
postPermission.addScope("GET");
postPermission.addPolicy("Always Deny Policy");
permissions.scope().findById(postPermission.getId()).update(postPermission);
authorize = authzClient.authorization(token).authorize();
token = authorize.getToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertFalse(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "POST"), testResponse.clear());
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
postPermission.addScope("GET");
postPermission.addPolicy("Always Grant Policy");
permissions.scope().findById(postPermission.getId()).update(postPermission);
authorize = authzClient.authorization(token).authorize();
token = authorize.getToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "POST"), testResponse.clear());
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
postPermission.addScope("POST");
postPermission.addPolicy("Always Deny Policy");
permissions.scope().findById(postPermission.getId()).update(postPermission);
AuthorizationRequest request = new AuthorizationRequest();
request.addPermission(null, "GET");
authorize = authzClient.authorization(token).authorize(request);
token = authorize.getToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token), testResponse.clear());
assertTrue(context.isGranted());
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/resource-with-scope", token, "POST"), testResponse.clear());
assertFalse(context.isGranted());
}
@Test
public void testUsingSubjectToken() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
ResourceRepresentation resource = createResource(clientResource, "Resource Subject Token", "/api/check-subject-token");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy("Only User Policy");
PermissionsResource permissions = clientResource.authorization().permissions();
permissions.resource().create(permission).close();
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only.json", true);
AuthzTestUtils.TestResponse testResponse = new AuthzTestUtils.TestResponse();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/check-subject-token"), testResponse);
assertFalse(context.isGranted());
assertEquals(403, testResponse.getStatus());
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/check-subject-token", token), testResponse.clear());
assertTrue(context.isGranted());
}
@Test
public void testUsingInvalidToken() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
ResourceRepresentation resource = createResource(clientResource, "Resource Subject Invalid Token", "/api/check-subject-token");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy("Only User Policy");
PermissionsResource permissions = clientResource.authorization().permissions();
permissions.resource().create(permission).close();
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-bearer-only.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/check-subject-token", token), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
oauth.doLogout(response.getRefreshToken(), null);
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/check-subject-token", token), new AuthzTestUtils.TestResponse());
assertFalse(context.isGranted());
}
@Test
public void testLazyLoadPaths() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
for (int i = 0; i < 200; i++) {
ResourceRepresentation representation = new ResourceRepresentation();
representation.setType("test");
representation.setName("Resource " + i);
representation.setUri("/api/" + i);
jakarta.ws.rs.core.Response response = clientResource.authorization().resources().create(representation);
representation.setId(response.readEntity(ResourceRepresentation.class).getId());
response.close();
}
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName("Test Permission");
permission.setResourceType("test");
permission.addPolicy("Only User Policy");
PermissionsResource permissions = clientResource.authorization().permissions();
permissions.resource().create(permission).close();
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-no-lazyload.json", true);
assertEquals(205, policyEnforcer.getPaths().size());
policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-lazyload.json", true);
assertEquals(0, policyEnforcer.getPathMatcher().getPathCache().size());
assertEquals(0, policyEnforcer.getPaths().size());
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
String token = response.getAccessToken();
for (int i = 0; i < 101; i++) {
policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/" + i, token), new AuthzTestUtils.TestResponse());
}
assertEquals(101, policyEnforcer.getPathMatcher().getPathCache().size());
for (int i = 101; i < 200; i++) {
policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/" + i, token), new AuthzTestUtils.TestResponse());
}
assertEquals(200, policyEnforcer.getPathMatcher().getPathCache().size());
assertEquals(0, policyEnforcer.getPaths().size());
ResourceRepresentation resource = clientResource.authorization().resources()
.findByName("Root").get(0);
clientResource.authorization().resources().resource(resource.getId()).remove();
policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-lazyload-with-paths.json", true);
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api/0", token), new AuthzTestUtils.TestResponse());
assertTrue(context.isGranted());
}
@Test
public void testSetMethodConfigs() {
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
ResourceRepresentation representation = new ResourceRepresentation();
representation.setName(KeycloakModelUtils.generateId());
representation.setUris(Collections.singleton("/api-method/*"));
ResourcesResource resources = clientResource.authorization().resources();
jakarta.ws.rs.core.Response response = resources.create(representation);
representation.setId(response.readEntity(ResourceRepresentation.class).getId());
response.close();
try {
PolicyEnforcer policyEnforcer = AuthzTestUtils.createPolicyEnforcer("enforcer-paths-use-method-config.json", true);
oauth.realm(REALM_NAME);
oauth.clientId("public-client-test");
oauth.doLogin("marta", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokeResponse = oauth.doAccessTokenRequest(code, null);
String token = tokeResponse.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api-method/foo", token), new AuthzTestUtils.TestResponse());
// GET is disabled in the config
assertTrue(context.isGranted());
PolicyEnforcerConfig.PathConfig pathConfig = policyEnforcer.getPaths().get("/api-method/*");
assertNotNull(pathConfig);
List<PolicyEnforcerConfig.MethodConfig> methods = pathConfig.getMethods();
assertEquals(1, methods.size());
assertTrue(PolicyEnforcerConfig.ScopeEnforcementMode.DISABLED.equals(methods.get(0).getScopesEnforcementMode()));
// other verbs should be protected
context = policyEnforcer.enforce(AuthzTestUtils.createHttpRequest("/api-method/foo", token, "POST"), new AuthzTestUtils.TestResponse());
assertFalse(context.isGranted());
} finally {
resources.resource(representation.getId()).remove();
}
}
private void initAuthorizationSettings(ClientResource clientResource) {
if (clientResource.authorization().resources().findByName("Resource A").isEmpty()) {
JSPolicyRepresentation jsPolicy = new JSPolicyRepresentation();
jsPolicy.setName("Always Grant Policy");
jsPolicy.setType("script-scripts/default-policy.js");
clientResource.authorization().policies().js().create(jsPolicy).close();
RolePolicyRepresentation rolePolicy = new RolePolicyRepresentation();
rolePolicy.setName("Only User Policy");
rolePolicy.addRole("user");
clientResource.authorization().policies().role().create(rolePolicy).close();
createResource(clientResource, "Resource A", "/api/resourcea");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName("Resource A Permission");
permission.addResource("Resource A");
permission.addPolicy(jsPolicy.getName());
clientResource.authorization().permissions().resource().create(permission).close();
}
if (clientResource.authorization().resources().findByName("Resource B").isEmpty()) {
JSPolicyRepresentation policy = new JSPolicyRepresentation();
policy.setName("Always Deny Policy");
policy.setType("script-scripts/always-deny-policy.js");
clientResource.authorization().policies().js().create(policy).close();
createResource(clientResource, "Resource B", "/api/resourceb");
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName("Resource B Permission");
permission.addResource("Resource B");
permission.addPolicy(policy.getName());
clientResource.authorization().permissions().resource().create(permission).close();
}
if (clientResource.authorization().resources().findByName("Root").isEmpty()) {
createResource(clientResource, "Root", "/*");
}
}
private ResourceRepresentation createResource(ClientResource clientResource, String name, String uri, String... scopes) {
ResourceRepresentation representation = new ResourceRepresentation();
representation.setName(name);
representation.setUri(uri);
representation.setScopes(Arrays.asList(scopes).stream().map(ScopeRepresentation::new).collect(Collectors.toSet()));
jakarta.ws.rs.core.Response response = clientResource.authorization().resources().create(representation);
representation.setId(response.readEntity(ResourceRepresentation.class).getId());
response.close();
return representation;
}
private ClientResource getClientResource(String name) {
ClientsResource clients = realmsResouce().realm(REALM_NAME).clients();
ClientRepresentation representation = clients.findByClientId(name).get(0);
return clients.get(representation.getId());
}
}

View file

@ -1,18 +0,0 @@
#
# * Copyright 2018 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.
#
org.keycloak.testsuite.authz.admin.MyCustomCIPFactory

View file

@ -1,26 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"claim-information-point": {
"claims": {
"claim-b": "claim-b"
}
},
"paths": [
{
"path": "/api/resourcea",
"claim-information-point": {
"claims": {
"claim-a": "{request.parameter['claim-a']}"
},
"my-custom-cip": {
"claim-value": "test"
}
}
}
]
}

View file

@ -1,8 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
}
}

View file

@ -1,101 +0,0 @@
{
"realm": "test-realm-authz",
"auth-server-url": "http://localhost:8180/auth",
"resource": "test-app-authz",
"credentials": {
"secret": "secret"
},
"paths": [
{
"path": "/claims-provider",
"methods": [
{
"method": "POST",
"scopes": [
"create"
]
}
],
"claim-information-point": {
"claims": {
"claim-from-request-parameter": "{request.parameter['a']}",
"claim-from-header": "{request.header['b']}",
"claim-from-cookie": "{request.cookie['c']}",
"claim-from-remoteAddr": "{request.remoteAddr}",
"claim-from-method": "{request.method}",
"claim-from-uri": "{request.uri}",
"claim-from-relativePath": "{request.relativePath}",
"claim-from-secure": "{request.secure}",
"claim-from-json-body-object": "{request.body['/a/b/c']}",
"claim-from-json-body-array": "{request.body['/d/1']}",
"claim-from-json-body-number": "{request.body['/e/number']}",
"claim-from-body": "{request.body}",
"claim-from-static-value": "static value",
"claim-from-multiple-static-value": ["static", "value"],
"param-replace-multiple-placeholder": "Test {keycloak.access_token['/custom_claim/0']} and {request.parameter['a']} "
}
}
},
{
"path": "/claims-from-body-json-object",
"methods": [
{
"method": "POST",
"scopes": [
"create"
]
}
],
"claim-information-point": {
"claims": {
"individualRoles": "{request.body['/Individual/individualRoles']}"
}
}
},
{
"path": "/http-post-claim-provider",
"claim-information-point": {
"http": {
"claims": {
"claim-a": "/a",
"claim-d": "/d",
"claim-d0": "/d/0",
"claim-d-all": ["/d/0", "/d/1"]
},
"url": "http://localhost:8989/post-claim-information-provider",
"method": "POST",
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"header-b": ["header-b-value1", "header-b-value2"],
"Authorization": "Bearer {keycloak.access_token}"
},
"parameters": {
"param-a": ["param-a-value1", "param-a-value2"],
"param-subject": "{keycloak.access_token['/sub']}",
"param-user-name": "{keycloak.access_token['/preferred_username']}",
"param-other-claims": "{keycloak.access_token['/custom_claim']}"
}
}
}
},
{
"path": "/http-get-claim-provider",
"claim-information-point": {
"http": {
"url": "http://localhost:8989/get-claim-information-provider",
"method": "get",
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"header-b": ["header-b-value1", "header-b-value2"],
"Authorization": "Bearer {keycloak.access_token}"
},
"parameters": {
"param-a": ["param-a-value1", "param-a-value2"],
"param-subject": "{keycloak.access_token['/sub']}",
"param-user-name": "{keycloak.access_token['/preferred_username']}"
}
}
}
}
]
}

View file

@ -1,28 +0,0 @@
{
"realm": "test-realm-authz",
"auth-server-url": "http://localhost:8180/auth",
"resource": "test-app-authz",
"credentials": {
"secret": "secret"
},
"paths": [
{
"path": "/v1/product/*",
"methods": [
{
"method": "POST",
"scopes": [
"create"
]
}
],
"claim-information-point": {
"claims": {
"claim-a": "{request.parameter['a']}",
"claim-b": "{request.header['b']}",
"claim-c": "{request.cookie['c']}"
}
}
}
]
}

View file

@ -1,54 +0,0 @@
{
"realm": "test-realm-authz",
"auth-server-url": "http://localhost:8180/auth",
"resource": "test-app-authz",
"credentials": {
"secret": "secret"
},
"paths": [
{
"path": "/v1/product/*",
"methods": [
{
"method": "POST",
"scopes": [
"create"
]
}
]
},
{
"path": "/v1/product/*",
"methods": [
{
"method": "GET",
"scopes": [
"view"
]
}
]
},
{
"path": "/v1/product/*",
"methods": [
{
"method": "PUT",
"scopes": [
"update"
]
}
]
},
{
"path": "/v1/product/*",
"methods": [
{
"method": "DELETE",
"scopes": [
"delete"
]
}
]
}
]
}

View file

@ -1,15 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"paths": [
{
"name": "Resource B",
"path": "/api/resource/public",
"enforcement-mode": "DISABLED"
}
]
}

View file

@ -1,15 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"enforcement-mode": "DISABLED",
"paths": [
{
"name": "Resource B",
"path": "/api/resource/public"
}
]
}

View file

@ -1,27 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"path-cache": {
"lifespan": 1
},
"paths": [
{
"name": "Resource B",
"path": "/api/resource/public",
"enforcement-mode": "DISABLED"
},
{
"name": "Nonexistent",
"path": "/api/resource/all-public/*",
"enforcement-mode": "DISABLED"
},
{
"name": "Static Test Resource",
"path": "/api/any-resource/test"
}
]
}

View file

@ -1,26 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"paths": [
{
"path": "/api/bank/account/{id}/withdrawal",
"methods": [
{
"method": "POST",
"scopes": [
"withdrawal"
]
}
],
"claim-information-point": {
"claims": {
"withdrawal.amount": "{request.parameter['withdrawal.amount']}"
}
}
}
]
}

View file

@ -1,15 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"lazy-load-paths": true,
"paths": [
{
"path": "/disabled",
"enforcement-mode": "DISABLED"
}
]
}

View file

@ -1,9 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"lazy-load-paths": true
}

View file

@ -1,9 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"http-method-as-scope": true
}

View file

@ -1,8 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
}
}

View file

@ -1,9 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"on-deny-redirect-to": "/accessDenied"
}

View file

@ -1,23 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-test",
"credentials": {
"secret": "secret"
},
"lazy-load-paths": true,
"paths": [
{
"path": "/api-method/*",
"methods": [
{
"method": "GET",
"scopes": [
"withdrawal"
],
"scopes-enforcement-mode": "DISABLED"
}
]
}
]
}

View file

@ -1,20 +0,0 @@
{
"realm": "authz-test",
"resource": "resource-server-test",
"auth-server-url": "http://localhost:8180/auth",
"credentials": {
"secret": "secret"
},
"lazy-load-paths": true,
"paths": [
{
"name": "Root",
"path": "/*",
"enforcement-mode": "DISABLED"
},
{
"name": "Resource A",
"path": "/api/*"
}
]
}

View file

@ -1,27 +0,0 @@
{
"realm": "authz-test",
"auth-server-url": "http://localhost:8180/auth",
"resource": "resource-server-uma-test",
"credentials": {
"secret": "secret"
},
"user-managed-access": {},
"paths": [
{
"path": "/api/bank/account/{id}/withdrawal",
"methods": [
{
"method": "POST",
"scopes": [
"withdrawal"
]
}
],
"claim-information-point": {
"claims": {
"withdrawal.amount": "{request.parameter['withdrawal.amount']}"
}
}
}
]
}

View file

@ -1,48 +0,0 @@
{
"id": "test-realm-authz",
"realm": "test-realm-authz",
"enabled": true,
"sslRequired": "external",
"requiredCredentials": [ "password" ],
"users": [
{
"username": "service-account-test-app-authz",
"enabled": true,
"serviceAccountClientId": "test-app-authz",
"clientRoles": {
"test-app-authz" : ["uma_protection"]
}
}
],
"clients": [
{
"clientId": "test-app-authz",
"enabled": true,
"baseUrl": "/test-app-authz",
"adminUrl": "/test-app-authz",
"bearerOnly": false,
"authorizationSettings": {
"allowRemoteResourceManagement": true,
"policyEnforcementMode": "ENFORCING",
"resources": [
{
"name": "Product Resource",
"uri": "/v1/product/*",
"scopes": [
{
"name": "view",
"name": "create",
"name": "delete",
"name": "update"
}
]
}
]
},
"redirectUris": [
"/test-app-authz/*"
],
"secret": "secret"
}
]
}

View file

@ -95,9 +95,5 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId> <artifactId>keycloak-adapter-spi</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer-tests</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>