Removing policy-enforcer from Keycloak repository
closes #32191 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
967893d3cf
commit
cd947ce3bc
54 changed files with 12 additions and 5092 deletions
|
@ -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>
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 + "]"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -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("]", "");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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>
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"realm": "authz-test",
|
|
||||||
"auth-server-url": "http://localhost:8180/auth",
|
|
||||||
"resource": "resource-server-test",
|
|
||||||
"credentials": {
|
|
||||||
"secret": "secret"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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']}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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']}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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']}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"realm": "authz-test",
|
|
||||||
"auth-server-url": "http://localhost:8180/auth",
|
|
||||||
"resource": "resource-server-test",
|
|
||||||
"credentials": {
|
|
||||||
"secret": "secret"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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/*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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']}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue