[KEYCLOAK-4902] - Refactoring and improvements to processing of authz requests

This commit is contained in:
Pedro Igor 2018-07-27 23:22:26 -03:00
parent 65f51b7b83
commit 80e5227bcd
50 changed files with 1343 additions and 825 deletions

View file

@ -17,6 +17,7 @@
*/
package org.keycloak.adapters.authorization;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -151,7 +152,7 @@ public abstract class AbstractPolicyEnforcer {
}
boolean hasPermission = false;
List<Permission> grantedPermissions = authorization.getPermissions();
Collection<Permission> grantedPermissions = authorization.getPermissions();
for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {

View file

@ -18,7 +18,7 @@
package org.keycloak.adapters.authorization;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -78,8 +78,8 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
List<Permission> grantedPermissions = authorization.getPermissions();
List<Permission> newPermissions = newAuthorization.getPermissions();
Collection<Permission> grantedPermissions = authorization.getPermissions();
Collection<Permission> newPermissions = newAuthorization.getPermissions();
for (Permission newPermission : newPermissions) {
if (!grantedPermissions.contains(newPermission)) {

View file

@ -17,10 +17,15 @@
*/
package org.keycloak.authorization.policy.provider.aggregated;
import java.util.List;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.policy.evaluation.Evaluation;
@ -34,31 +39,40 @@ public class AggregatePolicyProvider implements PolicyProvider {
@Override
public void evaluate(Evaluation evaluation) {
//TODO: need to detect deep recursions
DecisionResultCollector decision = new DecisionResultCollector() {
@Override
protected void onComplete(List<Result> results) {
if (results.isEmpty()) {
evaluation.deny();
} else {
Result result = results.iterator().next();
if (Effect.PERMIT.equals(result.getEffect())) {
protected void onComplete(Result result) {
if (isGranted(result.getResults().iterator().next())) {
evaluation.grant();
}
} else {
evaluation.deny();
}
}
};
Policy policy = evaluation.getPolicy();
AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
Policy policy = evaluation.getPolicy();
DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation);
Map<Policy, Map<Object, Decision.Effect>> decisionCache = defaultEvaluation.getDecisionCache();
ResourcePermission permission = evaluation.getPermission();
policy.getAssociatedPolicies().forEach(associatedPolicy -> {
for (Policy associatedPolicy : policy.getAssociatedPolicies()) {
Map<Object, Decision.Effect> decisions = decisionCache.computeIfAbsent(associatedPolicy, p -> new HashMap<>());
Decision.Effect effect = decisions.get(permission);
DefaultEvaluation eval = new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision, authorization, decisionCache);
if (effect == null) {
PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType());
policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision, authorization));
});
decision.onComplete();
policyProvider.evaluate(eval);
eval.denyIfNoEffect();
decisions.put(permission, eval.getEffect());
} else {
eval.setEffect(effect);
}
}
decision.onComplete(permission);
}
@Override

View file

@ -19,6 +19,9 @@ package org.keycloak.authorization.policy.provider.js;
import java.util.function.BiFunction;
import javax.script.ScriptContext;
import javax.script.SimpleScriptContext;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
@ -40,14 +43,14 @@ class JSPolicyProvider implements PolicyProvider {
public void evaluate(Evaluation evaluation) {
Policy policy = evaluation.getPolicy();
AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
final EvaluatableScriptAdapter adapter = evaluatableScript.apply(authorization, policy);
EvaluatableScriptAdapter adapter = evaluatableScript.apply(authorization, policy);
try {
//how to deal with long running scripts -> timeout?
adapter.eval(bindings -> {
bindings.put("script", adapter.getScriptModel());
bindings.put("$evaluation", evaluation);
});
SimpleScriptContext context = new SimpleScriptContext();
context.setAttribute("$evaluation", evaluation, ScriptContext.ENGINE_SCOPE);
adapter.eval(context);
}
catch (Exception e) {
throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e);

View file

@ -67,6 +67,12 @@ public class ScriptCache {
public EvaluatableScriptAdapter computeIfAbsent(String id, Function<String, EvaluatableScriptAdapter> function) {
try {
EvaluatableScriptAdapter adapter = removeIfExpired(cache.get(id));
if (adapter != null) {
return adapter;
}
if (parkForWriteAndCheckInterrupt()) {
return null;
}
@ -83,20 +89,6 @@ public class ScriptCache {
}
}
public EvaluatableScriptAdapter 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()) {
@ -132,16 +124,6 @@ public class ScriptCache {
return false;
}
private boolean parkForReadAndCheckInterrupt() {
while (writing.get()) {
LockSupport.parkNanos(1L);
if (Thread.interrupted()) {
return true;
}
}
return false;
}
private static final class CacheEntry {
final String key;

View file

@ -17,34 +17,42 @@
package org.keycloak.authorization.policy.provider.permission;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AbstractPermissionProvider implements PolicyProvider {
public AbstractPermissionProvider() {
}
public abstract class AbstractPermissionProvider implements PolicyProvider {
@Override
public void evaluate(Evaluation evaluation) {
if (!(evaluation instanceof DefaultEvaluation)) {
throw new IllegalArgumentException("Unexpected evaluation instance type [" + evaluation.getClass() + "]");
}
Policy policy = evaluation.getPolicy();
AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation);
Map<Policy, Map<Object, Decision.Effect>> decisionCache = defaultEvaluation.getDecisionCache();
Policy policy = evaluation.getPolicy();
ResourcePermission permission = evaluation.getPermission();
policy.getAssociatedPolicies().forEach(associatedPolicy -> {
Map<Object, Decision.Effect> decisions = decisionCache.computeIfAbsent(associatedPolicy, p -> new HashMap<>());
Decision.Effect effect = decisions.get(permission);
if (effect == null) {
PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType());
DefaultEvaluation.class.cast(evaluation).setPolicy(associatedPolicy);
policyProvider.evaluate(evaluation);
defaultEvaluation.setPolicy(associatedPolicy);
policyProvider.evaluate(defaultEvaluation);
evaluation.denyIfNoEffect();
decisions.put(permission, defaultEvaluation.getEffect());
} else {
defaultEvaluation.setEffect(effect);
}
});
}

View file

@ -16,9 +16,36 @@
*/
package org.keycloak.authorization.policy.provider.permission;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ResourcePolicyProvider extends AbstractPermissionProvider {
@Override
public void evaluate(Evaluation evaluation) {
DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation);
Map<Policy, Map<Object, Decision.Effect>> decisionCache = defaultEvaluation.getDecisionCache();
Policy policy = defaultEvaluation.getParentPolicy();
Map<Object, Decision.Effect> decisions = decisionCache.computeIfAbsent(policy, p -> new HashMap<>());
ResourcePermission permission = evaluation.getPermission();
Decision.Effect effect = decisions.get(permission.getResource());
if (effect != null) {
defaultEvaluation.setEffect(effect);
return;
}
super.evaluate(evaluation);
decisions.put(permission.getResource(), defaultEvaluation.getEffect());
}
}

View file

@ -16,9 +16,45 @@
*/
package org.keycloak.authorization.policy.provider.permission;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ScopePolicyProvider extends AbstractPermissionProvider {
@Override
public void evaluate(Evaluation evaluation) {
DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation);
Map<Policy, Map<Object, Decision.Effect>> decisionCache = defaultEvaluation.getDecisionCache();
Policy policy = defaultEvaluation.getParentPolicy();
Map<Object, Decision.Effect> decisions = decisionCache.computeIfAbsent(policy, p -> new HashMap<>());
ResourcePermission permission = evaluation.getPermission();
for (Scope scope : permission.getScopes()) {
Decision.Effect effect = decisions.get(scope);
if (effect != null) {
defaultEvaluation.setEffect(effect);
}
}
Decision.Effect decision = defaultEvaluation.getEffect();
if (decision == null) {
super.evaluate(evaluation);
for (Scope scope : policy.getScopes()) {
decisions.put(scope, defaultEvaluation.getEffect());
}
}
}
}

View file

@ -22,9 +22,9 @@ import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -113,7 +113,7 @@ public class AuthorizationContext {
return Collections.emptyList();
}
return Collections.unmodifiableList(authorization.getPermissions());
return Collections.unmodifiableList(new ArrayList<>(authorization.getPermissions()));
}
public boolean isGranted() {

View file

@ -22,9 +22,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.idm.authorization.Permission;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -86,13 +86,13 @@ public class AccessToken extends IDToken {
public static class Authorization implements Serializable {
@JsonProperty("permissions")
private List<Permission> permissions;
private Collection<Permission> permissions;
public List<Permission> getPermissions() {
public Collection<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
public void setPermissions(Collection<Permission> permissions) {
this.permissions = permissions;
}
}

View file

@ -186,6 +186,7 @@ public class AuthorizationRequest {
private Boolean includeResourceName;
private Integer limit;
private String responseMode;
public Boolean getIncludeResourceName() {
if (includeResourceName == null) {
@ -205,5 +206,13 @@ public class AuthorizationRequest {
public void setLimit(Integer limit) {
this.limit = limit;
}
public void setResponseMode(String responseMode) {
this.responseMode = responseMode;
}
public String getResponseMode() {
return responseMode;
}
}
}

View file

@ -61,6 +61,9 @@ public class Permission {
}
public String getResourceId() {
if (resourceId == null || "".equals(resourceId.trim())) {
return null;
}
return this.resourceId;
}

View file

@ -35,7 +35,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class StoreFactoryCacheManager extends CacheManager {
private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
private static final Logger logger = Logger.getLogger(StoreFactoryCacheManager.class);
public StoreFactoryCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);

View file

@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -756,8 +757,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
}
return Arrays.asList(policy);
},
(revision, policies) -> new PolicyListQuery(revision, cacheKey, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}, (revision, policies) -> new PolicyListQuery(revision, cacheKey, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, null);
if (result.isEmpty()) {
return null;
@ -780,40 +780,62 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
public List<Policy> findByResource(String resourceId, String resourceServerId) {
String cacheKey = getPolicyByResource(resourceId, resourceServerId);
return cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResource(resourceId, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceId, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceId, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, null);
}
@Override
public void findByResource(String resourceId, String resourceServerId, Consumer<Policy> consumer) {
String cacheKey = getPolicyByResource(resourceId, resourceServerId);
cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResource(resourceId, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceId, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer);
}
@Override
public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
String cacheKey = getPolicyByResourceType(resourceType, resourceServerId);
return cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResourceType(resourceType, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceType, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceType, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, null);
}
@Override
public void findByResourceType(String resourceType, String resourceServerId, Consumer<Policy> consumer) {
String cacheKey = getPolicyByResourceType(resourceType, resourceServerId);
cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResourceType(resourceType, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceType, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer);
}
@Override
public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
if (scopeIds == null) return null;
List<Policy> result = new ArrayList<>();
Set<Policy> result = new HashSet<>();
for (String id : scopeIds) {
String cacheKey = getPolicyByScope(id, resourceServerId);
result.addAll(cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId));
result.addAll(cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, null));
}
return result;
return new ArrayList<>(result);
}
@Override
public List<Policy> findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId) {
if (scopeIds == null) return null;
List<Policy> result = new ArrayList<>();
Set<Policy> result = new HashSet<>();
for (String id : scopeIds) {
String cacheKey = getPolicyByResourceScope(id, resourceId, resourceServerId);
result.addAll(cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceId, resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId));
result.addAll(cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceId, resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, null));
}
return result;
return new ArrayList<>(result);
}
@Override
public void findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId, Consumer<Policy> consumer) {
for (String id : scopeIds) {
String cacheKey = getPolicyByResourceScope(id, resourceId, resourceServerId);
cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceId, resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer);
}
}
@Override
@ -826,7 +848,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
return getPolicyStoreDelegate().findDependentPolicies(id, resourceServerId);
}
private <R, Q extends PolicyQuery> List<R> cacheQuery(String cacheKey, Class<Q> queryType, Supplier<List<R>> resultSupplier, BiFunction<Long, List<R>, Q> querySupplier, String resourceServerId) {
private <R, Q extends PolicyQuery> List<R> cacheQuery(String cacheKey, Class<Q> queryType, Supplier<List<R>> resultSupplier, BiFunction<Long, List<R>, Q> querySupplier, String resourceServerId, Consumer<R> consumer) {
Q query = cache.get(cacheKey, queryType);
if (query != null) {
logger.tracev("cache hit for key: {0}", cacheKey);
@ -838,11 +860,34 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
if (invalidations.contains(cacheKey)) return model;
query = querySupplier.apply(loaded, model);
cache.addRevisioned(query, startupRevision);
if (consumer != null) {
for (R policy: model) {
consumer.accept(policy);
}
}
return model;
} else if (query.isInvalid(invalidations)) {
return resultSupplier.get();
List<R> policies = resultSupplier.get();
if (consumer != null) {
for (R policy : policies) {
consumer.accept(policy);
}
}
return policies;
} else {
return query.getPolicies().stream().map(resourceId -> (R) findById(resourceId, resourceServerId)).collect(Collectors.toList());
Set<String> policies = query.getPolicies();
if (consumer != null) {
for (String id : policies) {
consumer.accept((R) findById(id, resourceServerId));
}
return null;
}
return policies.stream().map(resourceId -> (R) findById(resourceId, resourceServerId)).collect(Collectors.toList());
}
}
}

View file

@ -23,6 +23,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
@ -188,40 +189,52 @@ public class JPAPolicyStore implements PolicyStore {
@Override
public List<Policy> findByResource(final String resourceId, String resourceServerId) {
List<Policy> result = new LinkedList<>();
findByResource(resourceId, resourceServerId, result::add);
return result;
}
@Override
public void findByResource(String resourceId, String resourceServerId, Consumer<Policy> consumer) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByResource", String.class);
query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("resourceId", resourceId);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Policy> list = new LinkedList<>();
for (String id : result) {
Policy policy = provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId);
if (Objects.nonNull(policy)) {
list.add(policy);
}
}
return list;
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
query.getResultList().stream()
.map(id -> policyStore.findById(id, resourceServerId))
.filter(Objects::nonNull)
.forEach(consumer::accept);
}
@Override
public List<Policy> findByResourceType(final String resourceType, String resourceServerId) {
List<Policy> result = new LinkedList<>();
findByResourceType(resourceType, resourceServerId, result::add);
return result;
}
@Override
public void findByResourceType(String resourceType, String resourceServerId, Consumer<Policy> consumer) {
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByResourceType", String.class);
query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("type", resourceType);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Policy> list = new LinkedList<>();
for (String id : result) {
Policy policy = provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId);
if (Objects.nonNull(policy)) {
list.add(policy);
}
}
return list;
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
query.getResultList().stream()
.map(id -> policyStore.findById(id, resourceServerId))
.filter(Objects::nonNull)
.forEach(consumer::accept);
}
@Override
@ -250,10 +263,15 @@ public class JPAPolicyStore implements PolicyStore {
@Override
public List<Policy> findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId) {
if (scopeIds==null || scopeIds.isEmpty()) {
return Collections.emptyList();
List<Policy> result = new LinkedList<>();
findByScopeIds(scopeIds, resourceId, resourceServerId, result::add);
return result;
}
@Override
public void findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId, Consumer<Policy> consumer) {
// Use separate subquery to handle DB2 and MSSSQL
TypedQuery<String> query;
@ -268,15 +286,12 @@ public class JPAPolicyStore implements PolicyStore {
query.setParameter("scopeIds", scopeIds);
query.setParameter("serverId", resourceServerId);
List<String> result = query.getResultList();
List<Policy> list = new LinkedList<>();
for (String id : result) {
Policy policy = provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId);
if (Objects.nonNull(policy)) {
list.add(policy);
}
}
return list;
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
query.getResultList().stream()
.map(id -> policyStore.findById(id, resourceServerId))
.filter(Objects::nonNull)
.forEach(consumer::accept);
}
@Override

View file

@ -22,6 +22,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.keycloak.authorization.model.PermissionTicket;
@ -30,7 +31,7 @@ import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.evaluator.Evaluators;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PermissionTicketStore;
@ -74,18 +75,18 @@ import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentati
*/
public final class AuthorizationProvider implements Provider {
private final DefaultPolicyEvaluator policyEvaluator;
private final PolicyEvaluator policyEvaluator;
private StoreFactory storeFactory;
private StoreFactory storeFactoryDelegate;
private final Map<String, PolicyProviderFactory> policyProviderFactories;
private final KeycloakSession keycloakSession;
private final RealmModel realm;
public AuthorizationProvider(KeycloakSession session, RealmModel realm, Map<String, PolicyProviderFactory> policyProviderFactories) {
public AuthorizationProvider(KeycloakSession session, RealmModel realm, Map<String, PolicyProviderFactory> policyProviderFactories, PolicyEvaluator policyEvaluator) {
this.keycloakSession = session;
this.realm = realm;
this.policyProviderFactories = policyProviderFactories;
this.policyEvaluator = new DefaultPolicyEvaluator(this);
this.policyEvaluator = policyEvaluator;
}
/**
@ -95,7 +96,7 @@ public final class AuthorizationProvider implements Provider {
* @return a {@link Evaluators} instance
*/
public Evaluators evaluators() {
return new Evaluators(policyEvaluator);
return new Evaluators(this);
}
/**
@ -169,6 +170,10 @@ public final class AuthorizationProvider implements Provider {
return realm;
}
public PolicyEvaluator getPolicyEvaluator() {
return policyEvaluator;
}
@Override
public void close() {
@ -380,6 +385,11 @@ public final class AuthorizationProvider implements Provider {
return policyStore.findByResource(resourceId, resourceServerId);
}
@Override
public void findByResource(String resourceId, String resourceServerId, Consumer<Policy> consumer) {
policyStore.findByResource(resourceId, resourceServerId, consumer);
}
@Override
public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
return policyStore.findByResourceType(resourceType, resourceServerId);
@ -395,6 +405,11 @@ public final class AuthorizationProvider implements Provider {
return policyStore.findByScopeIds(scopeIds, resourceId, resourceServerId);
}
@Override
public void findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId, Consumer<Policy> consumer) {
policyStore.findByScopeIds(scopeIds, resourceId, resourceServerId, consumer);
}
@Override
public List<Policy> findByType(String type, String resourceServerId) {
return policyStore.findByType(type, resourceServerId);
@ -404,6 +419,11 @@ public final class AuthorizationProvider implements Provider {
public List<Policy> findDependentPolicies(String id, String resourceServerId) {
return policyStore.findDependentPolicies(id, resourceServerId);
}
@Override
public void findByResourceType(String type, String id, Consumer<Policy> policyConsumer) {
policyStore.findByResourceType(type, id, policyConsumer);
}
};
}

View file

@ -18,6 +18,7 @@
package org.keycloak.authorization;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.Evaluation;
/**
@ -38,4 +39,7 @@ public interface Decision<D extends Evaluation> {
default void onComplete() {
}
default void onComplete(ResourcePermission permission) {
}
}

View file

@ -18,12 +18,12 @@
package org.keycloak.authorization.permission.evaluator;
import java.util.List;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import java.util.Collection;
/**
* A factory for the different {@link PermissionEvaluator} implementations.
*
@ -31,17 +31,13 @@ import org.keycloak.authorization.policy.evaluation.EvaluationContext;
*/
public final class Evaluators {
private final DefaultPolicyEvaluator policyEvaluator;
private final AuthorizationProvider authorizationProvider;
public Evaluators(DefaultPolicyEvaluator policyEvaluator) {
this.policyEvaluator = policyEvaluator;
public Evaluators(AuthorizationProvider authorizationProvider) {
this.authorizationProvider = authorizationProvider;
}
public PermissionEvaluator from(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
return schedule(permissions, evaluationContext);
}
public PermissionEvaluator schedule(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
return new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator);
public PermissionEvaluator from(Collection<ResourcePermission> permissions, EvaluationContext evaluationContext) {
return new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, authorizationProvider);
}
}

View file

@ -17,16 +17,21 @@
*/
package org.keycloak.authorization.permission.evaluator;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Map;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
import org.keycloak.authorization.policy.evaluation.DecisionPermissionCollector;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.Permission;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -36,19 +41,24 @@ class IterablePermissionEvaluator implements PermissionEvaluator {
private final Iterator<ResourcePermission> permissions;
private final EvaluationContext executionContext;
private final PolicyEvaluator policyEvaluator;
private final AuthorizationProvider authorizationProvider;
IterablePermissionEvaluator(Iterator<ResourcePermission> permissions, EvaluationContext executionContext, PolicyEvaluator policyEvaluator) {
IterablePermissionEvaluator(Iterator<ResourcePermission> permissions, EvaluationContext executionContext, AuthorizationProvider authorizationProvider) {
this.permissions = permissions;
this.executionContext = executionContext;
this.policyEvaluator = policyEvaluator;
this.authorizationProvider = authorizationProvider;
this.policyEvaluator = authorizationProvider.getPolicyEvaluator();
}
@Override
public Decision evaluate(Decision decision) {
try {
Map<Policy, Map<Object, Decision.Effect>> decisionCache = new HashMap<>();
while (this.permissions.hasNext()) {
this.policyEvaluator.evaluate(this.permissions.next(), this.executionContext, decision);
this.policyEvaluator.evaluate(this.permissions.next(), authorizationProvider, executionContext, decision, decisionCache);
}
decision.onComplete();
} catch (Throwable cause) {
decision.onError(cause);
@ -57,21 +67,11 @@ class IterablePermissionEvaluator implements PermissionEvaluator {
}
@Override
public List<Result> evaluate() {
AtomicReference<List<Result>> result = new AtomicReference<>();
public Collection<Permission> evaluate(ResourceServer resourceServer, AuthorizationRequest request) {
DecisionPermissionCollector decision = new DecisionPermissionCollector(authorizationProvider, resourceServer, request);
evaluate(new DecisionResultCollector() {
@Override
public void onError(Throwable cause) {
throw new RuntimeException("Failed to evaluate permissions", cause);
}
evaluate(decision);
@Override
protected void onComplete(List<Result> results) {
result.set(results);
}
});
return result.get();
return decision.results();
}
}

View file

@ -17,10 +17,12 @@
*/
package org.keycloak.authorization.permission.evaluator;
import java.util.List;
import java.util.Collection;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.Permission;
/**
* An {@link PermissionEvaluator} represents a source of {@link org.keycloak.authorization.permission.ResourcePermission}, responsible for emitting these permissions
@ -31,5 +33,5 @@ import org.keycloak.authorization.policy.evaluation.Result;
public interface PermissionEvaluator {
<D extends Decision> D evaluate(D decision);
List<Result> evaluate();
Collection<Permission> evaluate(ResourceServer resourceServer, AuthorizationRequest request);
}

View file

@ -0,0 +1,99 @@
/*
* 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.authorization.policy.evaluation;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class AbstractDecisionCollector implements Decision<DefaultEvaluation> {
protected final Map<ResourcePermission, Result> results = new LinkedHashMap<>();
@Override
public void onDecision(DefaultEvaluation evaluation) {
Policy parentPolicy = evaluation.getParentPolicy();
if (parentPolicy != null) {
results.computeIfAbsent(evaluation.getPermission(), permission -> new Result(permission, evaluation)).policy(parentPolicy).policy(evaluation.getPolicy(), evaluation.getEffect());
} else {
results.computeIfAbsent(evaluation.getPermission(), permission -> new Result(permission, evaluation)).setStatus(evaluation.getEffect());
}
}
@Override
public void onComplete() {
onComplete(results.values());
}
@Override
public void onComplete(ResourcePermission permission) {
onComplete(results.get(permission));
}
protected void onComplete(Result result) {
}
protected void onComplete(Collection<Result> permissions) {
}
protected boolean isGranted(Result.PolicyResult policyResult) {
Policy policy = policyResult.getPolicy();
DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
switch (decisionStrategy) {
case AFFIRMATIVE:
for (Result.PolicyResult decision : policyResult.getAssociatedPolicies()) {
if (Effect.PERMIT.equals(decision.getEffect())) {
return true;
}
}
return false;
case CONSENSUS:
int grantCount = 0;
int denyCount = policy.getAssociatedPolicies().size();
for (Result.PolicyResult decision : policyResult.getAssociatedPolicies()) {
if (decision.getEffect().equals(Effect.PERMIT)) {
grantCount++;
denyCount--;
}
}
return grantCount > denyCount;
default:
// defaults to UNANIMOUS
for (Result.PolicyResult decision : policyResult.getAssociatedPolicies()) {
if (Effect.DENY.equals(decision.getEffect())) {
return false;
}
}
return true;
}
}
}

View file

@ -0,0 +1,186 @@
/*
* 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.authorization.policy.evaluation;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class DecisionPermissionCollector extends AbstractDecisionCollector {
private final AuthorizationProvider authorizationProvider;
private final ResourceServer resourceServer;
private final AuthorizationRequest request;
private final List<Permission> permissions = new ArrayList<>();
public DecisionPermissionCollector(AuthorizationProvider authorizationProvider, ResourceServer resourceServer, AuthorizationRequest request) {
this.authorizationProvider = authorizationProvider;
this.resourceServer = resourceServer;
this.request = request;
}
@Override
public void onComplete(Result result) {
ResourcePermission permission = result.getPermission();
Resource resource = permission.getResource();
Set<Scope> grantedScopes = new HashSet<>();
if (Effect.PERMIT.equals(result.getEffect())) {
if (resource != null) {
grantedScopes.addAll(resource.getScopes());
} else {
grantedScopes.addAll(permission.getScopes());
}
grantPermission(authorizationProvider, permissions, permission, grantedScopes, resourceServer, request, result);
} else {
Set<Scope> deniedScopes = new HashSet<>();
List<Result.PolicyResult> userManagedPermissions = new ArrayList<>();
Collection<Result.PolicyResult> permissionResults = new ArrayList<>(result.getResults());
Iterator<Result.PolicyResult> iterator = permissionResults.iterator();
while (iterator.hasNext()) {
Result.PolicyResult policyResult = iterator.next();
Policy policy = policyResult.getPolicy();
Set<Scope> policyScopes = policy.getScopes();
if (isGranted(policyResult)) {
if (isScopePermission(policy)) {
for (Scope scope : permission.getScopes()) {
if (policyScopes.contains(scope)) {
// try to grant any scope from a scope-based permission
grantedScopes.add(scope);
}
}
} else if (isResourcePermission(policy)) {
// we assume that all requested scopes should be granted given that we are processing a resource-based permission.
// Later they will be filtered based on any denied scope, if any.
// TODO: we could probably provide a configuration option to let users decide whether or not a resource-based permission should grant all scopes associated with the resource.
grantedScopes.addAll(permission.getScopes());
}
if (resource != null && resource.isOwnerManagedAccess() && "uma".equals(policy.getType())) {
userManagedPermissions.add(policyResult);
}
iterator.remove();
} else {
if (isResourcePermission(policy)) {
deniedScopes.addAll(resource.getScopes());
} else {
deniedScopes.addAll(policyScopes);
}
}
}
// remove any scope denied from the list of granted scopes
grantedScopes.removeAll(deniedScopes);
if (!userManagedPermissions.isEmpty()) {
Set<Scope> scopes = new HashSet<>();
for (Result.PolicyResult userManagedPermission : userManagedPermissions) {
grantedScopes.addAll(userManagedPermission.getPolicy().getScopes());
}
if (!scopes.isEmpty()) {
grantedScopes.clear();
}
// deny scopes associated with a resource that are not explicitly granted by the user
if (!resource.getScopes().isEmpty() && scopes.isEmpty()) {
deniedScopes.addAll(resource.getScopes());
} else {
permissionResults.clear();
}
}
if (!grantedScopes.isEmpty() || (permissionResults.isEmpty() && deniedScopes.isEmpty())) {
grantPermission(authorizationProvider, permissions, permission, grantedScopes, resourceServer, request, result);
}
}
}
public Collection<Permission> results() {
return permissions;
}
@Override
public void onError(Throwable cause) {
throw new RuntimeException("Failed to evaluate permissions", cause);
}
protected void grantPermission(AuthorizationProvider authorizationProvider, List<Permission> permissions, ResourcePermission permission, Set<Scope> grantedScopes, ResourceServer resourceServer, AuthorizationRequest request, Result result) {
Set<String> scopeNames = grantedScopes.stream().map(Scope::getName).collect(Collectors.toSet());
Resource resource = permission.getResource();
if (resource != null) {
permissions.add(createPermission(resource, scopeNames, permission.getClaims(), request));
} else if (!grantedScopes.isEmpty()) {
ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore();
List<Resource> resources = resourceStore.findByScope(grantedScopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId());
if (resources.isEmpty()) {
permissions.add(createPermission(null, scopeNames, permission.getClaims(), request));
} else {
for (Resource grantedResource : resources) {
permissions.add(createPermission(grantedResource, scopeNames, permission.getClaims(), request));
}
}
}
}
private Permission createPermission(Resource resource, Set<String> scopes, Map<String, Set<String>> claims, AuthorizationRequest request) {
AuthorizationRequest.Metadata metadata = null;
if (request != null) {
metadata = request.getMetadata();
}
if (resource != null) {
String resourceName = metadata == null || metadata.getIncludeResourceName() ? resource.getName() : null;
return new Permission(resource.getId(), resourceName, scopes, claims);
}
return new Permission(null, null, scopes, claims);
}
private static boolean isResourcePermission(Policy policy) {
return "resource".equals(policy.getType());
}
private static boolean isScopePermission(Policy policy) {
return "scope".equals(policy.getType());
}
}

View file

@ -1,51 +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.authorization.policy.evaluation;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class DecisionResult extends DecisionResultCollector {
protected List<Result> results;
protected Throwable error;
@Override
protected void onComplete(List<Result> results) {
this.results = results;
}
@Override
public void onError(Throwable cause) {
this.error = cause;
}
public boolean completed() {
return results != null && error == null;
}
public List<Result> getResults() {
return results;
}
public Throwable getError() {
return error;
}
}

View file

@ -18,99 +18,14 @@
package org.keycloak.authorization.policy.evaluation;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class DecisionResultCollector implements Decision<DefaultEvaluation> {
public abstract class DecisionResultCollector extends AbstractDecisionCollector {
private Map<ResourcePermission, Result> results = new LinkedHashMap<>();
@Override
public void onDecision(DefaultEvaluation evaluation) {
Policy parentPolicy = evaluation.getParentPolicy();
if (parentPolicy != null) {
results.computeIfAbsent(evaluation.getPermission(), Result::new).policy(parentPolicy).policy(evaluation.getPolicy()).setStatus(evaluation.getEffect());
} else {
results.computeIfAbsent(evaluation.getPermission(), Result::new).setStatus(evaluation.getEffect());
}
}
@Override
public void onComplete() {
for (Result result : results.values()) {
int deniedCount = result.getResults().size();
for (Result.PolicyResult policyResult : result.getResults()) {
if (isGranted(policyResult)) {
policyResult.setStatus(Effect.PERMIT);
deniedCount--;
} else {
policyResult.setStatus(Effect.DENY);
}
}
if (deniedCount == 0) {
onGrant(result);
} else {
onDeny(result);
}
}
onComplete(results.values().stream().collect(Collectors.toList()));
}
protected void onGrant(Result result) {
result.setStatus(Effect.PERMIT);
}
protected abstract void onComplete(List<Result> results);
protected void onDeny(Result result) {
result.setStatus(Effect.DENY);
}
private boolean isGranted(Result.PolicyResult policyResult) {
List<Result.PolicyResult> values = policyResult.getAssociatedPolicies();
int grantCount = 0;
int denyCount = policyResult.getPolicy().getAssociatedPolicies().size();
for (Result.PolicyResult decision : values) {
if (decision.getStatus().equals(Effect.PERMIT)) {
grantCount++;
denyCount--;
}
}
Policy policy = policyResult.getPolicy();
DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
if (decisionStrategy == null) {
decisionStrategy = DecisionStrategy.UNANIMOUS;
}
if (DecisionStrategy.AFFIRMATIVE.equals(decisionStrategy) && grantCount > 0) {
return true;
} else if (DecisionStrategy.UNANIMOUS.equals(decisionStrategy) && denyCount == 0) {
return true;
} else if (DecisionStrategy.CONSENSUS.equals(decisionStrategy)) {
if (grantCount > denyCount) {
return true;
}
}
return false;
}
}

View file

@ -18,6 +18,8 @@
package org.keycloak.authorization.policy.evaluation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -51,25 +53,30 @@ public class DefaultEvaluation implements Evaluation {
private Policy policy;
private final Policy parentPolicy;
private final AuthorizationProvider authorizationProvider;
private Map<Policy, Map<Object, Effect>> decisionCache;
private final Realm realm;
private Effect effect;
public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Decision decision, AuthorizationProvider authorizationProvider) {
this.permission = permission;
this.executionContext = executionContext;
this.parentPolicy = parentPolicy;
this.decision = decision;
this.authorizationProvider = authorizationProvider;
this.realm = createRealm();
public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Decision decision, AuthorizationProvider authorizationProvider, Map<Policy, Map<Object, Decision.Effect>> decisionCache) {
this(permission, executionContext, parentPolicy, null, decision, authorizationProvider, decisionCache);
}
public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision, AuthorizationProvider authorizationProvider) {
this(permission, executionContext, parentPolicy, policy, decision, authorizationProvider, null);
}
public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AuthorizationProvider authorizationProvider) {
this(permission, executionContext, null, null, decision, authorizationProvider, Collections.emptyMap());
}
public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision, AuthorizationProvider authorizationProvider, Map<Policy, Map<Object, Decision.Effect>> decisionCache) {
this.permission = permission;
this.executionContext = executionContext;
this.parentPolicy = parentPolicy;
this.policy = policy;
this.decision = decision;
this.authorizationProvider = authorizationProvider;
this.decisionCache = decisionCache;
this.realm = createRealm();
}
@ -131,6 +138,10 @@ public class DefaultEvaluation implements Evaluation {
return effect;
}
public Map<Policy, Map<Object, Effect>> getDecisionCache() {
return decisionCache;
}
@Override
public void denyIfNoEffect() {
if (this.effect == null) {
@ -265,4 +276,12 @@ public class DefaultEvaluation implements Evaluation {
public void setPolicy(Policy policy) {
this.policy = policy;
}
public void setEffect(Effect effect) {
if (Effect.PERMIT.equals(effect)) {
grant();
} else {
deny();
}
}
}

View file

@ -19,12 +19,10 @@
package org.keycloak.authorization.policy.evaluation;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.keycloak.authorization.AuthorizationProvider;
@ -45,163 +43,73 @@ import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
*/
public class DefaultPolicyEvaluator implements PolicyEvaluator {
private final AuthorizationProvider authorization;
private final StoreFactory storeFactory;
private final PolicyStore policyStore;
private final ResourceStore resourceStore;
public DefaultPolicyEvaluator(AuthorizationProvider authorization) {
this.authorization = authorization;
storeFactory = this.authorization.getStoreFactory();
policyStore = storeFactory.getPolicyStore();
resourceStore = storeFactory.getResourceStore();
}
@Override
public void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision) {
public void evaluate(ResourcePermission permission, AuthorizationProvider authorizationProvider, EvaluationContext executionContext, Decision decision, Map<Policy, Map<Object, Decision.Effect>> decisionCache) {
StoreFactory storeFactory = authorizationProvider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
ResourceStore resourceStore = storeFactory.getResourceStore();
ResourceServer resourceServer = permission.getResourceServer();
PolicyEnforcementMode enforcementMode = resourceServer.getPolicyEnforcementMode();
if (PolicyEnforcementMode.DISABLED.equals(enforcementMode)) {
createEvaluation(permission, executionContext, decision, null).grant();
DefaultEvaluation evaluation = new DefaultEvaluation(permission, executionContext, decision, authorizationProvider);
evaluation.grant();
decision.onComplete(permission);
return;
}
AtomicBoolean verified = new AtomicBoolean(false);
Consumer<Policy> consumer = createDecisionConsumer(permission, executionContext, decision, verified);
Set<Policy> verified = new HashSet<>();
Consumer<Policy> policyConsumer = createPolicyEvaluator(permission, authorizationProvider, executionContext, decision, verified, decisionCache);
Resource resource = permission.getResource();
List<Scope> scopes = permission.getScopes();
if (resource != null) {
evaluatePolicies(() -> policyStore.findByResource(resource.getId(), resourceServer.getId()), consumer);
policyStore.findByResource(resource.getId(), resourceServer.getId(), policyConsumer);
if (resource.getType() != null) {
evaluatePolicies(() -> {
List<Policy> policies = policyStore.findByResourceType(resource.getType(), resourceServer.getId());
policyStore.findByResourceType(resource.getType(), resourceServer.getId(), policyConsumer);
if (!resource.getOwner().equals(resourceServer.getId())) {
for (Resource typedResource : resourceStore.findByType(resource.getType(), resourceServer.getId())) {
policies.addAll(policyStore.findByResource(typedResource.getId(), resourceServer.getId()));
policyStore.findByResource(typedResource.getId(), resourceServer.getId(), policyConsumer);
}
}
}
}
return policies;
}, consumer);
}
}
List<Scope> scopes = permission.getScopes();
if (!scopes.isEmpty()) {
evaluatePolicies(() -> policyStore.findByScopeIds(scopes.stream().map(Scope::getId).collect(Collectors.toList()), null, resourceServer.getId()), consumer);
policyStore.findByScopeIds(scopes.stream().map(Scope::getId).collect(Collectors.toList()), null, resourceServer.getId(), policyConsumer);
}
if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode) && !verified.get()) {
createEvaluation(permission, executionContext, decision, null).grant();
}
}
private void evaluatePolicies(Supplier<List<Policy>> supplier, Consumer<Policy> consumer) {
List<Policy> policies = supplier.get();
if (!policies.isEmpty()) {
policies.forEach(consumer);
}
}
private Consumer<Policy> createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicBoolean verified) {
return (parentPolicy) -> {
if (!hasRequestedScopes(permission, parentPolicy)) {
if (!verified.isEmpty()) {
decision.onComplete(permission);
return;
}
PolicyProvider policyProvider = authorization.getProvider(parentPolicy.getType());
if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode)) {
DefaultEvaluation evaluation = new DefaultEvaluation(permission, executionContext, decision, authorizationProvider);
evaluation.grant();
decision.onComplete(permission);
}
}
private Consumer<Policy> createPolicyEvaluator(ResourcePermission permission, AuthorizationProvider authorizationProvider, EvaluationContext executionContext, Decision decision, Set<Policy> verified, Map<Policy, Map<Object, Decision.Effect>> decisionCache) {
return parentPolicy -> {
if (!verified.add(parentPolicy)) {
return;
}
PolicyProvider policyProvider = authorizationProvider.getProvider(parentPolicy.getType());
if (policyProvider == null) {
throw new RuntimeException("Unknown parentPolicy provider for type [" + parentPolicy.getType() + "].");
}
DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy);
policyProvider.evaluate(evaluation);
verified.compareAndSet(false, true);
policyProvider.evaluate(new DefaultEvaluation(permission, executionContext, parentPolicy, decision, authorizationProvider, decisionCache));
};
}
private DefaultEvaluation createEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, Policy parentPolicy) {
return new DefaultEvaluation(permission, executionContext, parentPolicy, decision, authorization);
}
private boolean hasRequestedScopes(final ResourcePermission permission, final Policy policy) {
if (permission.getScopes().isEmpty()) {
return true;
}
Resource resourcePermission = permission.getResource();
Set<Resource> policyResources = policy.getResources();
if (resourcePermission != null && !policyResources.isEmpty()) {
if (!policyResources.stream().filter(resource -> {
Iterator<Resource> policyResourceType = policy.getResources().iterator();
Resource policyResource = policyResourceType.hasNext() ? policyResourceType.next() : null;
return resource.getId().equals(resourcePermission.getId()) || (policyResourceType != null && policyResource.getType() != null && policyResource.getType().equals(resourcePermission.getType()));
}).findFirst().isPresent()) {
return false;
}
}
Set<Scope> scopes = new HashSet<>(policy.getScopes());
if (scopes.isEmpty()) {
Set<Resource> resources = new HashSet<>();
resources.addAll(policyResources);
for (Resource resource : resources) {
scopes.addAll(resource.getScopes());
}
if (!resources.isEmpty() && scopes.isEmpty()) {
return false;
}
if (scopes.isEmpty()) {
Resource resource = permission.getResource();
String type = resource.getType();
if (type != null) {
List<Resource> resourcesByType = resourceStore.findByType(type, resource.getResourceServer().getId());
for (Resource resourceType : resourcesByType) {
if (resourceType.getOwner().equals(resource.getResourceServer().getId())) {
resources.add(resourceType);
}
}
}
}
for (Resource resource : resources) {
scopes.addAll(resource.getScopes());
}
}
for (Scope givenScope : scopes) {
for (Scope scope : permission.getScopes()) {
if (givenScope.getId().equals(scope.getId())) {
return true;
}
}
}
if (policyResources.isEmpty() && scopes.isEmpty()) {
String defaultResourceType = policy.getConfig().get("defaultResourceType");
if (defaultResourceType == null) {
return false;
}
return defaultResourceType.equals(permission.getResource().getType());
}
return false;
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.authorization.policy.evaluation;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -41,16 +42,16 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PermissionTicketAwareDecisionResultCollector extends DecisionResultCollector {
public class PermissionTicketAwareDecisionResultCollector extends DecisionPermissionCollector {
private final AuthorizationRequest request;
private PermissionTicketToken ticket;
private final Identity identity;
private ResourceServer resourceServer;
private final AuthorizationProvider authorization;
private List<Result> results;
public PermissionTicketAwareDecisionResultCollector(AuthorizationRequest request, PermissionTicketToken ticket, Identity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
super(authorization, resourceServer, request);
this.request = request;
this.ticket = ticket;
this.identity = identity;
@ -168,13 +169,4 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult
}
}
}
@Override
protected void onComplete(List<Result> results) {
this.results = results;
}
public List<Result> results() {
return results;
}
}

View file

@ -18,9 +18,13 @@
package org.keycloak.authorization.policy.evaluation;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import java.util.Map;
/**
* <p>A {@link PolicyEvaluator} evaluates authorization policies based on a given {@link ResourcePermission}, sending
* the results to a {@link Decision} point through the methods defined in that interface.
@ -34,5 +38,5 @@ public interface PolicyEvaluator {
*
* @param decision a {@link Decision} point to where notifications events will be delivered during the evaluation
*/
void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision);
void evaluate(ResourcePermission permission, AuthorizationProvider authorizationProvider, EvaluationContext executionContext, Decision decision, Map<Policy, Map<Object, Decision.Effect>> decisionCache);
}

View file

@ -22,8 +22,9 @@ import org.keycloak.authorization.Decision.Effect;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -31,33 +32,29 @@ import java.util.List;
public class Result {
private final ResourcePermission permission;
private List<PolicyResult> results = new ArrayList<>();
private Effect status;
private final Map<String, PolicyResult> results = new HashMap<>();
private final Evaluation evaluation;
private Effect status = Effect.DENY;
public Result(ResourcePermission permission) {
public Result(ResourcePermission permission, Evaluation evaluation) {
this.permission = permission;
this.evaluation = evaluation;
}
public ResourcePermission getPermission() {
return permission;
}
public List<PolicyResult> getResults() {
return results;
public Collection<PolicyResult> getResults() {
return results.values();
}
public Evaluation getEvaluation() {
return evaluation;
}
public PolicyResult policy(Policy policy) {
for (PolicyResult result : this.results) {
if (result.getPolicy().equals(policy)) {
return result;
}
}
PolicyResult policyResult = new PolicyResult(policy);
this.results.add(policyResult);
return policyResult;
return results.computeIfAbsent(policy.getId(), id -> new PolicyResult(policy));
}
public void setStatus(final Effect status) {
@ -71,50 +68,40 @@ public class Result {
public static class PolicyResult {
private final Policy policy;
private List<PolicyResult> associatedPolicies = new ArrayList<>();
private Effect status;
private final Map<String, PolicyResult> associatedPolicies = new HashMap<>();
private Effect effect = Effect.DENY;
public PolicyResult(Policy policy, Effect status) {
this.policy = policy;
this.effect = status;
}
public PolicyResult(Policy policy) {
this.policy = policy;
this(policy, Effect.DENY);
}
public PolicyResult status(Effect status) {
this.status = status;
return this;
}
public PolicyResult policy(Policy policy, Effect effect) {
PolicyResult result = associatedPolicies.computeIfAbsent(policy.getId(), id -> new PolicyResult(policy, effect));
public PolicyResult policy(Policy policy) {
return getPolicy(policy, this.associatedPolicies);
}
result.setEffect(effect);
private PolicyResult getPolicy(Policy policy, List<PolicyResult> results) {
for (PolicyResult result : results) {
if (result.getPolicy().equals(policy)) {
return result;
}
}
PolicyResult policyResult = new PolicyResult(policy);
results.add(policyResult);
return policyResult;
}
public Policy getPolicy() {
return policy;
}
public List<PolicyResult> getAssociatedPolicies() {
return associatedPolicies;
public Collection<PolicyResult> getAssociatedPolicies() {
return associatedPolicies.values();
}
public Effect getStatus() {
return status;
public Effect getEffect() {
return effect;
}
public void setStatus(final Effect status) {
this.status = status;
public void setEffect(final Effect status) {
this.effect = status;
}
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.authorization.store;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
@ -93,6 +94,8 @@ public interface PolicyStore {
*/
List<Policy> findByResource(String resourceId, String resourceServerId);
void findByResource(String resourceId, String resourceServerId, Consumer<Policy> consumer);
/**
* Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>type</code>.
*
@ -121,6 +124,8 @@ public interface PolicyStore {
*/
List<Policy> findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId);
void findByScopeIds(List<String> scopeIds, String resourceId, String resourceServerId, Consumer<Policy> consumer);
/**
* Returns a list of {@link Policy} with the given <code>type</code>.
*
@ -138,4 +143,6 @@ public interface PolicyStore {
* @return a list of policies that depends on the a policy with the given identifier
*/
List<Policy> findDependentPolicies(String id, String resourceServerId);
void findByResourceType(String type, String id, Consumer<Policy> policyConsumer);
}

View file

@ -1,5 +1,7 @@
package org.keycloak.scripting;
import javax.script.ScriptContext;
import org.keycloak.models.ScriptModel;
/**
@ -11,4 +13,5 @@ public interface EvaluatableScriptAdapter {
ScriptModel getScriptModel();
Object eval(ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException;
Object eval(ScriptContext context) throws ScriptExecutionException;
}

View file

@ -23,13 +23,13 @@ import java.util.List;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
import org.keycloak.provider.ProviderFactory;
/**
@ -38,6 +38,7 @@ import org.keycloak.provider.ProviderFactory;
public class DefaultAuthorizationProviderFactory implements AuthorizationProviderFactory {
private Map<String, PolicyProviderFactory> policyProviderFactories;
private PolicyEvaluator policyEvaluator = new DefaultPolicyEvaluator();
@Override
public AuthorizationProvider create(KeycloakSession session) {
@ -65,7 +66,7 @@ public class DefaultAuthorizationProviderFactory implements AuthorizationProvide
@Override
public AuthorizationProvider create(KeycloakSession session, RealmModel realm) {
return new AuthorizationProvider(session, realm, policyProviderFactories);
return new AuthorizationProvider(session, realm, policyProviderFactories, policyEvaluator);
}
private Map<String, PolicyProviderFactory> configurePolicyProviderFactories(KeycloakSessionFactory keycloakSessionFactory) {

View file

@ -21,7 +21,6 @@ package org.keycloak.authorization.admin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -37,6 +36,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.jboss.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder;
@ -47,23 +47,24 @@ import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DecisionPermissionCollector;
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.authorization.util.Permissions;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@ -78,6 +79,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
*/
public class PolicyEvaluationService {
private static final Logger logger = Logger.getLogger(PolicyEvaluationService.class);
private final AuthorizationProvider authorization;
private final AdminPermissionEvaluator auth;
private final ResourceServer resourceServer;
@ -117,14 +120,15 @@ public class PolicyEvaluationService {
return Response.ok(PolicyEvaluationResponseBuilder.build(evaluate(evaluationRequest, createEvaluationContext(evaluationRequest, identity), request), resourceServer, authorization, identity)).build();
} catch (Exception e) {
logger.error("Error while evaluating permissions", e);
throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, "Error while evaluating permissions.", Status.INTERNAL_SERVER_ERROR);
} finally {
identity.close();
}
}
private List<Result> evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext, AuthorizationRequest request) {
return authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization, request), evaluationContext).evaluate();
private EvaluationDecisionCollector evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext, AuthorizationRequest request) {
return authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization, request), evaluationContext).evaluate(new EvaluationDecisionCollector(authorization, resourceServer, request));
}
private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation, KeycloakIdentity identity) {
@ -171,7 +175,7 @@ public class PolicyEvaluationService {
if (resource.getId() != null) {
Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId(), resourceServer.getId());
return Arrays.asList(Permissions.createResourcePermissions(resourceModel, scopeNames, authorization, request)).stream();
return new ArrayList<>(Arrays.asList(Permissions.createResourcePermissions(resourceModel, scopeNames, authorization, request))).stream();
} else if (resource.getType() != null) {
return storeFactory.getResourceStore().findByType(resource.getType(), resourceServer.getId()).stream().map(resource1 -> Permissions.createResourcePermissions(resource1, scopeNames, authorization, request));
} else {
@ -180,7 +184,7 @@ public class PolicyEvaluationService {
List<ResourcePermission> collect = new ArrayList<>();
if (!scopes.isEmpty()) {
collect.addAll(scopes.stream().map(scope -> new ResourcePermission(null, Arrays.asList(scope), resourceServer)).collect(Collectors.toList()));
collect.addAll(scopes.stream().map(scope -> new ResourcePermission(null, new ArrayList<>(Arrays.asList(scope)), resourceServer)).collect(Collectors.toList()));
} else {
collect.addAll(Permissions.all(resourceServer, evaluationContext.getIdentity(), authorization, request));
}
@ -261,4 +265,31 @@ public class PolicyEvaluationService {
return new CloseableKeycloakIdentity(accessToken, keycloakSession, userSession);
}
public class EvaluationDecisionCollector extends DecisionPermissionCollector {
public EvaluationDecisionCollector(AuthorizationProvider authorizationProvider, ResourceServer resourceServer, AuthorizationRequest request) {
super(authorizationProvider, resourceServer, request);
}
@Override
protected boolean isGranted(Result.PolicyResult policyResult) {
if (super.isGranted(policyResult)) {
policyResult.setEffect(Effect.PERMIT);
return true;
}
return false;
}
@Override
protected void grantPermission(AuthorizationProvider authorizationProvider, List<Permission> permissions, ResourcePermission permission, Set<Scope> grantedScopes, ResourceServer resourceServer, AuthorizationRequest request, Result result) {
result.setStatus(Effect.PERMIT);
result.getPermission().getScopes().retainAll(grantedScopes);
super.grantPermission(authorizationProvider, permissions, permission, grantedScopes, resourceServer, request, result);
}
public Collection<Result> getResults() {
return results.values();
}
}
}

View file

@ -18,13 +18,13 @@ package org.keycloak.authorization.admin.representation;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.admin.PolicyEvaluationService;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.util.Permissions;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
@ -38,6 +38,7 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -52,13 +53,13 @@ import java.util.stream.Stream;
* @version $Revision: 1 $
*/
public class PolicyEvaluationResponseBuilder {
public static PolicyEvaluationResponse build(List<Result> results, ResourceServer resourceServer, AuthorizationProvider authorization, KeycloakIdentity identity) {
public static PolicyEvaluationResponse build(PolicyEvaluationService.EvaluationDecisionCollector decision, ResourceServer resourceServer, AuthorizationProvider authorization, KeycloakIdentity identity) {
PolicyEvaluationResponse response = new PolicyEvaluationResponse();
List<PolicyEvaluationResponse.EvaluationResultRepresentation> resultsRep = new ArrayList<>();
AccessToken accessToken = identity.getAccessToken();
AccessToken.Authorization authorizationData = new AccessToken.Authorization();
authorizationData.setPermissions(Permissions.permits(results, null, authorization, resourceServer));
authorizationData.setPermissions(decision.results());
accessToken.setAuthorization(authorizationData);
ClientModel clientModel = authorization.getRealm().getClientById(resourceServer.getId());
@ -69,6 +70,8 @@ public class PolicyEvaluationResponseBuilder {
response.setRpt(accessToken);
Collection<Result> results = decision.getResults();
if (results.stream().anyMatch(evaluationResult -> evaluationResult.getEffect().equals(Decision.Effect.DENY))) {
response.setStatus(DecisionEffect.DENY);
} else {
@ -217,7 +220,7 @@ public class PolicyEvaluationResponseBuilder {
policyResultRep.setPolicy(representation);
if (result.getStatus() == Decision.Effect.DENY) {
if (result.getEffect() == Decision.Effect.DENY) {
policyResultRep.setStatus(DecisionEffect.DENY);
policyResultRep.setScopes(representation.getScopes());
} else {

View file

@ -19,6 +19,7 @@ package org.keycloak.authorization.authorization;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@ -46,7 +47,6 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.PermissionTicketAwareDecisionResultCollector;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
@ -88,6 +88,9 @@ public class AuthorizationTokenService {
public static final String CLAIM_TOKEN_FORMAT_ID_TOKEN = "http://openid.net/specs/openid-connect-core-1_0.html#IDToken";
private static final Logger logger = Logger.getLogger(AuthorizationTokenService.class);
private static final String RESPONSE_MODE_DECISION = "decision";
private static final String RESPONSE_MODE_PERMISSIONS = "permissions";
private static final String RESPONSE_MODE_DECISION_RESULT = "result";
private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
static {
@ -125,28 +128,20 @@ public class AuthorizationTokenService {
});
}
private final TokenManager tokenManager;
private final EventBuilder event;
private final HttpRequest httpRequest;
private final AuthorizationProvider authorization;
private final Cors cors;
private static final AuthorizationTokenService INSTANCE = new AuthorizationTokenService();
public AuthorizationTokenService(AuthorizationProvider authorization, TokenManager tokenManager, EventBuilder event, HttpRequest httpRequest, Cors cors) {
this.tokenManager = tokenManager;
this.event = event;
this.httpRequest = httpRequest;
this.authorization = authorization;
this.cors = cors;
public static AuthorizationTokenService instance() {
return INSTANCE;
}
public Response authorize(AuthorizationRequest request) {
public Response authorize(KeycloakAuthorizationRequest request) {
if (request == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid authorization request.", Status.BAD_REQUEST);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_GRANT, "Invalid authorization request.", Status.BAD_REQUEST);
}
// it is not secure to allow public clients to push arbitrary claims because message can be tampered
if (isPublicClientRequestingEntitlementWithClaims(request)) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Public clients are not allowed to send claims", Status.FORBIDDEN);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_GRANT, "Public clients are not allowed to send claims", Status.FORBIDDEN);
}
try {
@ -154,35 +149,46 @@ public class AuthorizationTokenService {
request.setClaims(ticket.getClaims());
ResourceServer resourceServer = getResourceServer(ticket);
ResourceServer resourceServer = getResourceServer(ticket, request);
KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
List<Result> evaluation;
Collection<Permission> permissions;
if (ticket.getPermissions().isEmpty() && request.getRpt() == null) {
evaluation = evaluateAllPermissions(request, resourceServer, evaluationContext, identity);
} else if(!request.getPermissions().getPermissions().isEmpty()) {
evaluation = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity);
if (request.getTicket() != null) {
permissions = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity);
} else if (ticket.getPermissions().isEmpty() && request.getRpt() == null) {
permissions = evaluateAllPermissions(request, resourceServer, evaluationContext, identity);
} else {
evaluation = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity);
permissions = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity);
}
List<Permission> permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer);
if (isGranted(ticket, request, permissions)) {
ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId());
AuthorizationResponse response = createAuthorizationResponse(identity, permissions, request, targetClient);
AuthorizationProvider authorization = request.getAuthorization();
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
Metadata metadata = request.getMetadata();
String responseMode = metadata != null ? metadata.getResponseMode() : null;
return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response))
.allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient)
.allowedMethods(HttpMethod.POST)
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
if (responseMode != null) {
if (RESPONSE_MODE_DECISION.equals(metadata.getResponseMode())) {
Map<String, Object> responseClaims = new HashMap<>();
responseClaims.put(RESPONSE_MODE_DECISION_RESULT, true);
return createSuccessfulResponse(responseClaims, targetClient, request);
} else if (RESPONSE_MODE_PERMISSIONS.equals(metadata.getResponseMode())) {
return createSuccessfulResponse(permissions, targetClient, request);
} else {
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Invalid response_mode", Status.BAD_REQUEST);
}
} else {
return createSuccessfulResponse(createAuthorizationResponse(identity, permissions, request, targetClient), targetClient, request);
}
}
if (request.isSubmitRequest()) {
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN);
} else {
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN);
}
} catch (ErrorResponseException | CorsErrorResponseException cause) {
if (logger.isDebugEnabled()) {
@ -191,45 +197,55 @@ public class AuthorizationTokenService {
throw cause;
} catch (Exception cause) {
logger.error("Unexpected error while evaluating permissions", cause);
throw new CorsErrorResponseException(cors, OAuthErrorException.SERVER_ERROR, "Unexpected error while evaluating permissions", Status.INTERNAL_SERVER_ERROR);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.SERVER_ERROR, "Unexpected error while evaluating permissions", Status.INTERNAL_SERVER_ERROR);
}
}
private boolean isPublicClientRequestingEntitlementWithClaims(AuthorizationRequest request) {
return request.getClaimToken() != null && getKeycloakSession().getContext().getClient().isPublicClient() && request.getTicket() == null;
private Response createSuccessfulResponse(Object response, ClientModel targetClient, KeycloakAuthorizationRequest request) {
return Cors.add(request.getHttpRequest(), Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response))
.allowedOrigins(request.getKeycloakSession().getContext().getUri(), targetClient)
.allowedMethods(HttpMethod.POST)
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
private List<Result> evaluatePermissions(AuthorizationRequest authorizationRequest, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
private boolean isPublicClientRequestingEntitlementWithClaims(KeycloakAuthorizationRequest request) {
return request.getClaimToken() != null && request.getKeycloakSession().getContext().getClient().isPublicClient() && request.getTicket() == null;
}
private Collection<Permission> evaluatePermissions(KeycloakAuthorizationRequest request, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
AuthorizationProvider authorization = request.getAuthorization();
return authorization.evaluators()
.from(createPermissions(ticket, authorizationRequest, resourceServer, identity, authorization), evaluationContext)
.evaluate();
.from(createPermissions(ticket, request, resourceServer, identity, authorization), evaluationContext)
.evaluate(resourceServer, request);
}
private List<Result> evaluateUserManagedPermissions(AuthorizationRequest request, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
private Collection<Permission> evaluateUserManagedPermissions(KeycloakAuthorizationRequest request, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
AuthorizationProvider authorization = request.getAuthorization();
return authorization.evaluators()
.from(createPermissions(ticket, request, resourceServer, identity, authorization), evaluationContext)
.evaluate(new PermissionTicketAwareDecisionResultCollector(request, ticket, identity, resourceServer, authorization)).results();
}
private List<Result> evaluateAllPermissions(AuthorizationRequest request, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
private Collection<Permission> evaluateAllPermissions(KeycloakAuthorizationRequest request, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
AuthorizationProvider authorization = request.getAuthorization();
return authorization.evaluators()
.from(Permissions.all(resourceServer, identity, authorization, request), evaluationContext)
.evaluate();
.evaluate(resourceServer, request);
}
private AuthorizationResponse createAuthorizationResponse(KeycloakIdentity identity, List<Permission> entitlements, AuthorizationRequest request, ClientModel targetClient) {
KeycloakSession keycloakSession = getKeycloakSession();
private AuthorizationResponse createAuthorizationResponse(KeycloakIdentity identity, Collection<Permission> entitlements, KeycloakAuthorizationRequest request, ClientModel targetClient) {
KeycloakSession keycloakSession = request.getKeycloakSession();
AccessToken accessToken = identity.getAccessToken();
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
ClientModel client = getRealm().getClientByClientId(accessToken.getIssuedFor());
RealmModel realm = request.getRealm();
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(realm, accessToken.getSessionState());
ClientModel client = realm.getClientByClientId(accessToken.getIssuedFor());
AuthenticatedClientSessionModel clientSession = userSessionModel.getAuthenticatedClientSessionByClient(client.getId());
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession);
AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(getRealm(), clientSession.getClient(), event, keycloakSession, userSessionModel, clientSessionCtx)
TokenManager tokenManager = request.getTokenManager();
EventBuilder event = request.getEvent();
AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, clientSession.getClient(), event, keycloakSession, userSessionModel, clientSessionCtx)
.generateAccessToken()
.generateRefreshToken();
AccessToken rpt = responseBuilder.getAccessToken();
rpt.issuedFor(client.getClientId());
@ -262,7 +278,7 @@ public class AuthorizationTokenService {
Authorization previousAuthorization = previousRpt.getAuthorization();
if (previousAuthorization != null) {
List<Permission> previousPermissions = previousAuthorization.getPermissions();
Collection<Permission> previousPermissions = previousAuthorization.getPermissions();
if (previousPermissions != null) {
for (Permission previousPermission : previousPermissions) {
@ -276,7 +292,7 @@ public class AuthorizationTokenService {
return true;
}
private PermissionTicketToken getPermissionTicket(AuthorizationRequest request) {
private PermissionTicketToken getPermissionTicket(KeycloakAuthorizationRequest request) {
// if there is a ticket is because it is a UMA flow and the ticket was sent by the client after obtaining it from the target resource server
if (request.getTicket() != null) {
return verifyPermissionTicket(request);
@ -292,32 +308,33 @@ public class AuthorizationTokenService {
return permissions;
}
private ResourceServer getResourceServer(PermissionTicketToken ticket) {
private ResourceServer getResourceServer(PermissionTicketToken ticket, KeycloakAuthorizationRequest request) {
AuthorizationProvider authorization = request.getAuthorization();
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
String[] audience = ticket.getAudience();
if (audience == null || audience.length == 0) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "You must provide the audience", Status.BAD_REQUEST);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "You must provide the audience", Status.BAD_REQUEST);
}
ClientModel clientModel = getRealm().getClientByClientId(audience[0]);
ClientModel clientModel = request.getRealm().getClientByClientId(audience[0]);
if (clientModel == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Unknown resource server id.", Status.BAD_REQUEST);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Unknown resource server id.", Status.BAD_REQUEST);
}
ResourceServer resourceServer = resourceServerStore.findById(clientModel.getId());
if (resourceServer == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.BAD_REQUEST);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.BAD_REQUEST);
}
return resourceServer;
}
private KeycloakEvaluationContext createEvaluationContext(AuthorizationRequest authorizationRequest) {
String claimTokenFormat = authorizationRequest.getClaimTokenFormat();
private KeycloakEvaluationContext createEvaluationContext(KeycloakAuthorizationRequest request) {
String claimTokenFormat = request.getClaimTokenFormat();
if (claimTokenFormat == null) {
claimTokenFormat = CLAIM_TOKEN_FORMAT_ID_TOKEN;
@ -326,13 +343,13 @@ public class AuthorizationTokenService {
BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
if (evaluationContextProvider == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Claim token format [" + claimTokenFormat + "] not supported", Status.BAD_REQUEST);
throw new CorsErrorResponseException(request.getCors(), OAuthErrorException.INVALID_REQUEST, "Claim token format [" + claimTokenFormat + "] not supported", Status.BAD_REQUEST);
}
return evaluationContextProvider.apply(authorizationRequest, authorization);
return evaluationContextProvider.apply(request, request.getAuthorization());
}
private List<ResourcePermission> createPermissions(PermissionTicketToken ticket, AuthorizationRequest request, ResourceServer resourceServer, KeycloakIdentity identity, AuthorizationProvider authorization) {
private Collection<ResourcePermission> createPermissions(PermissionTicketToken ticket, KeycloakAuthorizationRequest request, ResourceServer resourceServer, KeycloakIdentity identity, AuthorizationProvider authorization) {
StoreFactory storeFactory = authorization.getStoreFactory();
Map<String, ResourcePermission> permissionsToEvaluate = new LinkedHashMap<>();
ResourceStore resourceStore = storeFactory.getResourceStore();
@ -340,30 +357,35 @@ public class AuthorizationTokenService {
Metadata metadata = request.getMetadata();
Integer limit = metadata != null ? metadata.getLimit() : null;
for (Permission requestedResource : ticket.getPermissions()) {
for (Permission permission : ticket.getPermissions()) {
if (limit != null && limit <= 0) {
break;
}
Set<String> requestedScopes = requestedResource.getScopes();
Set<String> requestedScopes = permission.getScopes();
if (requestedResource.getScopes() == null) {
if (permission.getScopes() == null) {
requestedScopes = new HashSet<>();
}
List<Resource> existingResources = new ArrayList<>();
String resourceId = permission.getResourceId();
if (requestedResource.getResourceId() != null) {
Resource resource = resourceStore.findById(requestedResource.getResourceId(), resourceServer.getId());
if (resourceId != null) {
Resource resource = null;
if (resourceId.indexOf('-') != -1) {
resource = resourceStore.findById(resourceId, resourceServer.getId());
}
if (resource != null) {
existingResources.add(resource);
} else {
String resourceName = requestedResource.getResourceId();
String resourceName = resourceId;
Resource ownerResource = resourceStore.findByName(resourceName, identity.getId(), resourceServer.getId());
if (ownerResource != null) {
requestedResource.setResourceId(ownerResource.getId());
permission.setResourceId(ownerResource.getId());
existingResources.add(ownerResource);
}
@ -371,7 +393,7 @@ public class AuthorizationTokenService {
Resource serverResource = resourceStore.findByName(resourceName, resourceServer.getId());
if (serverResource != null) {
requestedResource.setResourceId(serverResource.getId());
permission.setResourceId(serverResource.getId());
existingResources.add(serverResource);
}
}
@ -386,28 +408,28 @@ public class AuthorizationTokenService {
List<Scope> requestedScopesModel = requestedScopes.stream().map(s -> scopeStore.findByName(s, resourceServer.getId())).filter(Objects::nonNull).collect(Collectors.toList());
if (requestedResource.getResourceId() != null && !"".equals(requestedResource.getResourceId().trim()) && existingResources.isEmpty()) {
throw new CorsErrorResponseException(cors, "invalid_resource", "Resource with id [" + requestedResource.getResourceId() + "] does not exist.", Status.BAD_REQUEST);
if (resourceId != null && existingResources.isEmpty()) {
throw new CorsErrorResponseException(request.getCors(), "invalid_resource", "Resource with id [" + resourceId + "] does not exist.", Status.BAD_REQUEST);
}
if ((requestedResource.getScopes() != null && !requestedResource.getScopes().isEmpty()) && requestedScopesModel.isEmpty()) {
throw new CorsErrorResponseException(cors, "invalid_scope", "One of the given scopes " + requestedResource.getScopes() + " are invalid", Status.BAD_REQUEST);
if ((permission.getScopes() != null && !permission.getScopes().isEmpty()) && requestedScopesModel.isEmpty()) {
throw new CorsErrorResponseException(request.getCors(), "invalid_scope", "One of the given scopes " + permission.getScopes() + " are invalid", Status.BAD_REQUEST);
}
if (!existingResources.isEmpty()) {
for (Resource resource : existingResources) {
ResourcePermission permission = permissionsToEvaluate.get(resource.getId());
ResourcePermission perm = permissionsToEvaluate.get(resource.getId());
if (permission == null) {
permission = Permissions.createResourcePermissions(resource, requestedScopes, authorization, request);
permissionsToEvaluate.put(resource.getId(), permission);
if (perm == null) {
perm = Permissions.createResourcePermissions(resource, requestedScopes, authorization, request);
permissionsToEvaluate.put(resource.getId(), perm);
if (limit != null) {
limit--;
}
} else {
for (Scope scope : requestedScopesModel) {
if (!permission.getScopes().contains(scope)) {
permission.getScopes().add(scope);
if (!perm.getScopes().contains(scope)) {
perm.getScopes().add(scope);
}
}
}
@ -415,14 +437,16 @@ public class AuthorizationTokenService {
} else {
List<Resource> resources = resourceStore.findByScope(requestedScopesModel.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId());
if (resources.isEmpty()) {
permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", new ResourcePermission(null, requestedScopesModel, resourceServer, request.getClaims()));
} else {
for (Resource resource : resources) {
permissionsToEvaluate.put(resource.getId(), Permissions.createResourcePermissions(resource, requestedScopes, authorization, request));
if (limit != null) {
limit--;
}
}
permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", new ResourcePermission(null, requestedScopesModel, resourceServer, request.getClaims()));
}
}
}
@ -432,7 +456,7 @@ public class AuthorizationTokenService {
AccessToken.Authorization authorizationData = rpt.getAuthorization();
if (authorizationData != null) {
List<Permission> permissions = authorizationData.getPermissions();
Collection<Permission> permissions = authorizationData.getPermissions();
if (permissions != null) {
for (Permission grantedPermission : permissions) {
@ -478,30 +502,30 @@ public class AuthorizationTokenService {
}
}
return new ArrayList<>(permissionsToEvaluate.values());
return permissionsToEvaluate.values();
}
private PermissionTicketToken verifyPermissionTicket(AuthorizationRequest request) {
private PermissionTicketToken verifyPermissionTicket(KeycloakAuthorizationRequest request) {
String ticketString = request.getTicket();
if (ticketString == null || !Tokens.verifySignature(getKeycloakSession(), getRealm(), ticketString)) {
throw new CorsErrorResponseException(cors, "invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
if (ticketString == null || !Tokens.verifySignature(request.getKeycloakSession(), request.getRealm(), ticketString)) {
throw new CorsErrorResponseException(request.getCors(), "invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
}
try {
PermissionTicketToken ticket = new JWSInput(ticketString).readJsonContent(PermissionTicketToken.class);
if (!ticket.isActive()) {
throw new CorsErrorResponseException(cors, "invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
throw new CorsErrorResponseException(request.getCors(), "invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
}
return ticket;
} catch (JWSInputException e) {
throw new CorsErrorResponseException(cors, "invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN);
throw new CorsErrorResponseException(request.getCors(), "invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN);
}
}
private boolean isGranted(PermissionTicketToken ticket, AuthorizationRequest request, List<Permission> permissions) {
private boolean isGranted(PermissionTicketToken ticket, AuthorizationRequest request, Collection<Permission> permissions) {
List<Permission> requestedPermissions = ticket.getPermissions();
// denies in case a rpt was provided along with the authorization request but any requested permission was not granted
@ -512,11 +536,48 @@ public class AuthorizationTokenService {
return !permissions.isEmpty();
}
private KeycloakSession getKeycloakSession() {
return this.authorization.getKeycloakSession();
public static class KeycloakAuthorizationRequest extends AuthorizationRequest {
private final AuthorizationProvider authorization;
private final TokenManager tokenManager;
private final EventBuilder event;
private final HttpRequest httpRequest;
private final Cors cors;
public KeycloakAuthorizationRequest(AuthorizationProvider authorization, TokenManager tokenManager, EventBuilder event, HttpRequest request, Cors cors) {
this.authorization = authorization;
this.tokenManager = tokenManager;
this.event = event;
httpRequest = request;
this.cors = cors;
}
private RealmModel getRealm() {
TokenManager getTokenManager() {
return tokenManager;
}
EventBuilder getEvent() {
return event;
}
HttpRequest getHttpRequest() {
return httpRequest;
}
AuthorizationProvider getAuthorization() {
return authorization;
}
Cors getCors() {
return cors;
}
KeycloakSession getKeycloakSession() {
return getAuthorization().getKeycloakSession();
}
RealmModel getRealm() {
return getKeycloakSession().getContext().getRealm();
}
}
}

View file

@ -21,8 +21,6 @@ package org.keycloak.authorization.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -32,21 +30,17 @@ import java.util.stream.Collectors;
import javax.ws.rs.core.Response.Status;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision.Effect;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.services.ErrorResponseException;
/**
@ -54,8 +48,8 @@ import org.keycloak.services.ErrorResponseException;
*/
public final class Permissions {
public static List<ResourcePermission> permission(ResourceServer server, Resource resource, Scope scope) {
return Arrays.asList(new ResourcePermission(resource, Arrays.asList(scope), server));
public static ResourcePermission permission(ResourceServer server, Resource resource, Scope scope) {
return new ResourcePermission(resource, new ArrayList<>(Arrays.asList(scope)), server);
}
/**
@ -73,22 +67,40 @@ public final class Permissions {
List<ResourcePermission> permissions = new ArrayList<>();
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
Metadata metadata = request.getMetadata();
long limit = Long.MAX_VALUE;
if (metadata != null && metadata.getLimit() != null) {
limit = metadata.getLimit();
}
// obtain all resources where owner is the resource server
resourceStore.findByOwner(resourceServer.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request)));
resourceStore.findByOwner(resourceServer.getId(), resourceServer.getId()).stream().limit(limit).forEach(resource -> permissions.add(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request)));
// obtain all resources where owner is the current user
resourceStore.findByOwner(identity.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request)));
resourceStore.findByOwner(identity.getId(), resourceServer.getId()).stream().limit(limit).forEach(resource -> permissions.add(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request)));
// obtain all resources granted to the user via permission tickets (uma)
List<PermissionTicket> tickets = storeFactory.getPermissionTicketStore().findGranted(identity.getId(), resourceServer.getId());
if (!tickets.isEmpty()) {
Map<String, ResourcePermission> userManagedPermissions = new HashMap<>();
for (PermissionTicket ticket : tickets) {
userManagedPermissions.computeIfAbsent(ticket.getResource().getId(), id -> new ResourcePermission(ticket.getResource(), new ArrayList<>(), resourceServer, request.getClaims()));
ResourcePermission permission = userManagedPermissions.get(ticket.getResource().getId());
if (permission == null) {
userManagedPermissions.put(ticket.getResource().getId(), new ResourcePermission(ticket.getResource(), new ArrayList<>(), resourceServer, request.getClaims()));
limit--;
}
if (--limit <= 0) {
break;
}
}
permissions.addAll(userManagedPermissions.values());
}
return permissions;
}
@ -131,8 +143,7 @@ public final class Permissions {
return new ResourcePermission(resource, scopes, resource.getResourceServer(), request.getClaims());
}
public static List<ResourcePermission> createResourcePermissionsWithScopes(Resource resource, List<Scope> scopes, AuthorizationProvider authorization, AuthorizationRequest request) {
List<ResourcePermission> permissions = new ArrayList<>();
public static ResourcePermission createResourcePermissionsWithScopes(Resource resource, List<Scope> scopes, AuthorizationProvider authorization, AuthorizationRequest request) {
String type = resource.getType();
ResourceServer resourceServer = resource.getResourceServer();
@ -152,151 +163,6 @@ public final class Permissions {
});
}
permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer(), request.getClaims()));
return permissions;
}
public static List<Permission> permits(List<Result> evaluation, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
return permits(evaluation, null, authorizationProvider, resourceServer);
}
public static List<Permission> permits(List<Result> evaluation, Metadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
Map<String, Permission> permissions = new LinkedHashMap<>();
for (Result result : evaluation) {
Set<Scope> deniedScopes = new HashSet<>();
Set<Scope> grantedScopes = new HashSet<>();
boolean resourceDenied = false;
ResourcePermission permission = result.getPermission();
List<Result.PolicyResult> results = result.getResults();
List<Result.PolicyResult> userManagedPermissions = new ArrayList<>();
int deniedCount = results.size();
Resource resource = permission.getResource();
for (Result.PolicyResult policyResult : results) {
Policy policy = policyResult.getPolicy();
Set<Scope> policyScopes = policy.getScopes();
if (Effect.PERMIT.equals(policyResult.getStatus())) {
if (isScopePermission(policy)) {
for (Scope scope : permission.getScopes()) {
if (policyScopes.contains(scope)) {
// try to grant any scope from a scope-based permission
grantedScopes.add(scope);
}
}
} else if (isResourcePermission(policy)) {
// we assume that all requested scopes should be granted given that we are processing a resource-based permission.
// Later they will be filtered based on any denied scope, if any.
// TODO: we could probably provide a configuration option to let users decide whether or not a resource-based permission should grant all scopes associated with the resource.
grantedScopes.addAll(permission.getScopes());
} if (resource != null && resource.isOwnerManagedAccess() && "uma".equals(policy.getType())) {
userManagedPermissions.add(policyResult);
}
deniedCount--;
} else {
if (isScopePermission(policy)) {
// store all scopes associated with the scope-based permission
deniedScopes.addAll(policyScopes);
} else if (isResourcePermission(policy)) {
resourceDenied = true;
deniedScopes.addAll(resource.getScopes());
}
}
}
// remove any scope denied from the list of granted scopes
if (!deniedScopes.isEmpty()) {
grantedScopes.removeAll(deniedScopes);
}
for (Result.PolicyResult policyResult : userManagedPermissions) {
Policy policy = policyResult.getPolicy();
grantedScopes.addAll(policy.getScopes());
resourceDenied = false;
}
// if there are no policy results is because the permission didn't match any policy.
// In this case, if results is empty is because we are in permissive mode.
if (!results.isEmpty()) {
// update the current permission with the granted scopes
permission.getScopes().clear();
permission.getScopes().addAll(grantedScopes);
}
if (deniedCount == 0) {
result.setStatus(Effect.PERMIT);
grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata);
} else {
// if a full deny or resource denied or the requested scopes were denied
if (deniedCount == results.size() || resourceDenied || (!deniedScopes.isEmpty() && grantedScopes.isEmpty())) {
result.setStatus(Effect.DENY);
} else {
result.setStatus(Effect.PERMIT);
grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata);
}
}
}
return permissions.values().stream().collect(Collectors.toList());
}
private static boolean isResourcePermission(Policy policy) {
return "resource".equals(policy.getType());
}
private static boolean isScopePermission(Policy policy) {
return "scope".equals(policy.getType());
}
private static void grantPermission(AuthorizationProvider authorizationProvider, Map<String, Permission> permissions, ResourcePermission permission, ResourceServer resourceServer, Metadata metadata) {
List<Resource> resources = new ArrayList<>();
Resource resource = permission.getResource();
Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
if (resource != null) {
resources.add(resource);
} else {
List<Scope> permissionScopes = permission.getScopes();
if (!permissionScopes.isEmpty()) {
ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore();
resources.addAll(resourceStore.findByScope(permissionScopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()));
}
}
if (!resources.isEmpty()) {
for (Resource allowedResource : resources) {
String resourceId = allowedResource.getId();
String resourceName = metadata == null || metadata.getIncludeResourceName() ? allowedResource.getName() : null;
Permission evalPermission = permissions.get(allowedResource.getId());
if (evalPermission == null) {
evalPermission = new Permission(resourceId, resourceName, scopes, permission.getClaims());
permissions.put(resourceId, evalPermission);
}
if (scopes != null && !scopes.isEmpty()) {
Set<String> finalScopes = evalPermission.getScopes();
if (finalScopes == null) {
finalScopes = new HashSet();
evalPermission.setScopes(finalScopes);
}
for (String scopeName : scopes) {
if (!finalScopes.contains(scopeName)) {
finalScopes.add(scopeName);
}
}
}
}
} else {
Permission scopePermission = new Permission(null, null, scopes, permission.getClaims());
permissions.put(scopePermission.toString(), scopePermission);
}
return new ResourcePermission(resource, scopes, resource.getResourceServer(), request.getClaims());
}
}

View file

@ -26,7 +26,6 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.authorization.AuthorizationTokenService;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.authorization.util.Tokens;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
@ -1070,11 +1069,13 @@ public class TokenEndpoint {
}
}
AuthorizationRequest authorizationRequest = new AuthorizationRequest(formParams.getFirst("ticket"));
AuthorizationTokenService.KeycloakAuthorizationRequest authorizationRequest = new AuthorizationTokenService.KeycloakAuthorizationRequest(session.getProvider(AuthorizationProvider.class), tokenManager, event, this.request, cors);
authorizationRequest.setTicket(formParams.getFirst("ticket"));
authorizationRequest.setClaimToken(claimToken);
authorizationRequest.setClaimTokenFormat(claimTokenFormat);
authorizationRequest.setPct(formParams.getFirst("pct"));
String rpt = formParams.getFirst("rpt");
if (rpt != null) {
@ -1128,9 +1129,11 @@ public class TokenEndpoint {
metadata.setLimit(Integer.parseInt(responsePermissionsLimit));
}
metadata.setResponseMode(formParams.getFirst("response_mode"));
authorizationRequest.setMetadata(metadata);
return new AuthorizationTokenService(session.getProvider(AuthorizationProvider.class), tokenManager, event, request, cors).authorize(authorizationRequest);
return AuthorizationTokenService.instance().authorize(authorizationRequest);
}
// https://tools.ietf.org/html/rfc7636#section-4.1

View file

@ -2,6 +2,7 @@ package org.keycloak.scripting;
import javax.script.Bindings;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
@ -37,4 +38,13 @@ class CompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter
protected Object eval(final Bindings bindings) throws ScriptException {
return compiledScript.eval(bindings);
}
@Override
public Object eval(ScriptContext context) throws ScriptExecutionException {
try {
return compiledScript.eval(context);
} catch (ScriptException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,6 +1,7 @@
package org.keycloak.scripting;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
@ -36,4 +37,12 @@ class UncompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapte
return getEngine().eval(getCode(), bindings);
}
@Override
public Object eval(ScriptContext context) throws ScriptExecutionException {
try {
return getEngine().eval(getCode(), context);
} catch (ScriptException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -20,7 +20,6 @@ import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.common.DefaultEvaluationContext;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.common.UserModelIdentity;
@ -30,7 +29,6 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.permission.evaluator.PermissionEvaluator;
import org.keycloak.authorization.policy.evaluation.DecisionResult;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.util.Permissions;
@ -45,7 +43,7 @@ import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminAuth;
import java.util.List;
import java.util.Arrays;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -332,15 +330,8 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
RealmModel oldRealm = session.getContext().getRealm();
try {
session.getContext().setRealm(realm);
DecisionResult decisionCollector = new DecisionResult();
List<ResourcePermission> permissions = Permissions.permission(resourceServer, resource, scope);
PermissionEvaluator from = authz.evaluators().from(permissions, context);
from.evaluate(decisionCollector);
if (!decisionCollector.completed()) {
logger.error("Failed to run permission check", decisionCollector.getError());
return false;
}
return decisionCollector.getResults().get(0).getEffect() == Decision.Effect.PERMIT;
ResourcePermission permission = Permissions.permission(resourceServer, resource, scope);
return !authz.evaluators().from(Arrays.asList(permission), context).evaluate(resourceServer, null).isEmpty();
} finally {
session.getContext().setRealm(oldRealm);
}

View file

@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -33,8 +34,6 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
@ -42,7 +41,6 @@ import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
@ -176,7 +174,7 @@ public abstract class AbstractResourceServerTest extends AbstractAuthzTest {
}
}
protected void assertPermissions(List<Permission> permissions, String expectedResource, String... expectedScopes) {
protected void assertPermissions(Collection<Permission> permissions, String expectedResource, String... expectedScopes) {
Iterator<Permission> iterator = permissions.iterator();
while (iterator.hasNext()) {

View file

@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@ -174,7 +175,7 @@ public class AuthorizationTest extends AbstractAuthzTest {
AuthorizationResponse response = getAuthzClient().authorization(userName, password).authorize(request);
AccessToken token = toAccessToken(response.getToken());
Authorization authorization = token.getAuthorization();
return authorization.getPermissions();
return new ArrayList<>(authorization.getPermissions());
}
private void createResourcePermission(ResourceRepresentation resource, String... policies) {

View file

@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -127,7 +128,7 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
List<Permission> permissions = new ArrayList<>(authorization.getPermissions());
assertFalse(permissions.isEmpty());
assertEquals("Default Resource", permissions.get(0).getResourceName());

View file

@ -16,14 +16,17 @@
*/
package org.keycloak.testsuite.authz;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -44,9 +47,11 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
@ -76,14 +81,15 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
@Before
public void configureAuthorization() throws Exception {
createResourcesAndScopes();
RealmResource realm = getRealm();
ClientResource client = getClient(realm);
if (client.authorization().resources().findByName("Resource A").isEmpty()) {
createResourcesAndScopes();
createPolicies(realm, client);
createPermissions(client);
}
}
/**
* <p>Scope Read on Resource A has two conflicting permissions. One is granting access for Marta and the other for Kolo.
@ -91,24 +97,29 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
* <p>Scope Read should not be granted for Marta.
*/
@Test
public void testMartaCanAccessResourceAWithExecuteAndWrite() {
List<Permission> permissions = getEntitlements("marta", "password");
public void testMartaCanAccessResourceAWithExecuteAndWrite() throws Exception {
ClientResource client = getClient(getRealm());
AuthorizationResource authorization = client.authorization();
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.ENFORCING);
authorization.update(settings);
Collection<Permission> permissions = getEntitlements("marta", "password");
assertEquals(1, permissions.size());
for (Permission permission : new ArrayList<>(permissions)) {
String resourceSetName = permission.getResourceName();
switch (resourceSetName) {
case "Resource A":
assertEquals(2, permission.getScopes().size());
assertTrue(permission.getScopes().contains("execute"));
assertTrue(permission.getScopes().contains("write"));
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write"));
permissions.remove(permission);
break;
case "Resource C":
assertEquals(3, permission.getScopes().size());
assertTrue(permission.getScopes().contains("execute"));
assertTrue(permission.getScopes().contains("write"));
assertTrue(permission.getScopes().contains("read"));
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
default:
@ -119,7 +130,83 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
assertTrue(permissions.isEmpty());
}
private List<Permission> getEntitlements(String username, String password) {
@Test
public void testWithPermissiveMode() throws Exception {
ClientResource client = getClient(getRealm());
AuthorizationResource authorization = client.authorization();
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.PERMISSIVE);
authorization.update(settings);
Collection<Permission> permissions = getEntitlements("marta", "password");
assertEquals(3, permissions.size());
for (Permission permission : new ArrayList<>(permissions)) {
String resourceSetName = permission.getResourceName();
switch (resourceSetName) {
case "Resource A":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write"));
permissions.remove(permission);
break;
case "Resource C":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
case "Resource B":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
default:
fail("Unexpected permission for resource [" + resourceSetName + "]");
}
}
assertTrue(permissions.isEmpty());
}
@Test
public void testWithDisabledMode() throws Exception {
ClientResource client = getClient(getRealm());
AuthorizationResource authorization = client.authorization();
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.DISABLED);
authorization.update(settings);
Collection<Permission> permissions = getEntitlements("marta", "password");
assertEquals(3, permissions.size());
for (Permission permission : new ArrayList<>(permissions)) {
String resourceSetName = permission.getResourceName();
switch (resourceSetName) {
case "Resource A":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
case "Resource C":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
case "Resource B":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
default:
fail("Unexpected permission for resource [" + resourceSetName + "]");
}
}
assertTrue(permissions.isEmpty());
}
private Collection<Permission> getEntitlements(String username, String password) {
AuthzClient authzClient = getAuthzClient();
AuthorizationResponse response = authzClient.authorization(username, password).authorize();
AccessToken accessToken;
@ -147,7 +234,7 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
}
private void createPermissions(ClientResource client) throws IOException {
createResourcePermission("Resource C Only For Marta Permission", "Resource C", Arrays.asList("Only Marta Policy"), client);
createResourcePermission("Resource A Only For Marta Permission", "Resource A", Arrays.asList("Only Marta Policy"), client);
createScopePermission("Resource A Scope Read Only For Marta Permission", "Resource A", Arrays.asList("read"), Arrays.asList("Only Marta Policy"), client);
createScopePermission("Resource A Scope Read Only For Kolo Permission", "Resource A", Arrays.asList("read"), Arrays.asList("Only Kolo Policy"), client);
}

View file

@ -26,6 +26,7 @@ import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -273,7 +274,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
AuthorizationResponse response = getAuthzClient(configFile).authorization("marta", "password").authorize(request);
AccessToken rpt = toAccessToken(response.getToken());
List<Permission> permissions = rpt.getAuthorization().getPermissions();
List<Permission> permissions = new ArrayList<>(rpt.getAuthorization().getPermissions());
assertEquals(10, permissions.size());
@ -293,7 +294,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
response = getAuthzClient(configFile).authorization("marta", "password").authorize(request);
rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
permissions = new ArrayList<>(rpt.getAuthorization().getPermissions());
assertEquals(10, permissions.size());
@ -317,7 +318,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
response = getAuthzClient(configFile).authorization("marta", "password").authorize(request);
rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
permissions = new ArrayList<>(rpt.getAuthorization().getPermissions());
assertEquals(10, permissions.size());
assertEquals("Resource 16", permissions.get(0).getResourceName());
@ -340,7 +341,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
response = getAuthzClient(configFile).authorization("marta", "password").authorize(request);
rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
permissions = new ArrayList<>(rpt.getAuthorization().getPermissions());
assertEquals(5, permissions.size());
assertEquals("Resource 16", permissions.get(0).getResourceName());
@ -442,7 +443,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
authorization.resources().resource(resource.getId()).update(resource);
// the addition of a new scope invalidates the permission previously granted to the resource
// the addition of a new scope still grants access to resource and any scope
assertFalse(hasPermission("kolo", "password", resource.getId()));
accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken();
@ -486,6 +487,39 @@ public class EntitlementAPITest extends AbstractAuthzTest {
assertFalse(hasPermission("kolo", "password", resource.getId(), "Scope B"));
}
@Test
public void testObtainAllEntitlementsWithLimit() throws Exception {
org.keycloak.authorization.client.resource.AuthorizationResource authorizationResource = getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization("marta", "password");
AuthorizationResponse response = authorizationResource.authorize();
AccessToken accessToken = toAccessToken(response.getToken());
Authorization authorization = accessToken.getAuthorization();
assertTrue(authorization.getPermissions().size() >= 20);
AuthorizationRequest request = new AuthorizationRequest();
Metadata metadata = new Metadata();
metadata.setLimit(10);
request.setMetadata(metadata);
response = authorizationResource.authorize(request);
accessToken = toAccessToken(response.getToken());
authorization = accessToken.getAuthorization();
assertEquals(10, authorization.getPermissions().size());
metadata.setLimit(1);
request.setMetadata(metadata);
response = authorizationResource.authorize(request);
accessToken = toAccessToken(response.getToken());
authorization = accessToken.getAuthorization();
assertEquals(1, authorization.getPermissions().size());
}
@Test
public void testObtainAllEntitlementsInvalidResource() throws Exception {
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
@ -625,7 +659,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
List<Permission> permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
Collection<Permission> permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(2, permissions.size());
for (Permission grantedPermission : permissions) {
@ -697,7 +731,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
AuthorizationResponse response = getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization(accessToken).authorize(new AuthorizationRequest());
AccessToken rpt = toAccessToken(response.getToken());
Authorization authz = rpt.getAuthorization();
List<Permission> permissions = authz.getPermissions();
Collection<Permission> permissions = authz.getPermissions();
assertNotNull(permissions);
assertFalse(permissions.isEmpty());
@ -718,7 +752,7 @@ public class EntitlementAPITest extends AbstractAuthzTest {
private void assertResponse(Metadata metadata, Supplier<AuthorizationResponse> responseSupplier) {
AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getToken()).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertFalse(permissions.isEmpty());

View file

@ -21,7 +21,9 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -128,7 +130,7 @@ public class PermissionClaimTest extends AbstractAuthzTest {
assertNotNull(response.getToken());
AccessToken rpt = toAccessToken(response.getToken());
Authorization authorizationClaim = rpt.getAuthorization();
List<Permission> permissions = authorizationClaim.getPermissions();
List<Permission> permissions = new ArrayList<>(authorizationClaim.getPermissions());
assertEquals(1, permissions.size());
@ -164,7 +166,7 @@ public class PermissionClaimTest extends AbstractAuthzTest {
assertNotNull(response.getToken());
AccessToken rpt = toAccessToken(response.getToken());
Authorization authorizationClaim = rpt.getAuthorization();
List<Permission> permissions = authorizationClaim.getPermissions();
List<Permission> permissions = new ArrayList<>(authorizationClaim.getPermissions());
assertEquals(1, permissions.size());

View file

@ -664,6 +664,6 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
}
return baseAttributes;
}
}, policy, evaluation -> {}, authorization);
}, policy, evaluation -> {}, authorization, null);
}
}

View file

@ -23,6 +23,7 @@ import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import javax.ws.rs.client.Client;
@ -98,7 +99,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"}, new String[] {"ScopeC"});
AccessToken accessToken = toAccessToken(response.getToken());
AccessToken.Authorization authorization = accessToken.getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB", "ScopeC");
@ -110,7 +111,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"});
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
@ -132,7 +133,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", null, new String[] {"ScopeA", "ScopeB"});
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertFalse(response.isUpgraded());
assertNotNull(permissions);
@ -155,7 +156,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"});
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertFalse(response.isUpgraded());
assertNotNull(permissions);
@ -194,7 +195,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", resourceA.getId(), new String[] {"ScopeA", "ScopeB"});
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertFalse(response.isUpgraded());
assertNotNull(permissions);
@ -261,7 +262,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
AuthorizationResponse response = authorize("marta", "password", resourceA.getName(), new String[] {"READ"});
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertFalse(response.isUpgraded());
assertNotNull(permissions);
@ -303,7 +304,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
new PermissionRequest(resourceB.getName(), "ScopeC"));
String rpt = response.getToken();
AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB");
@ -324,7 +325,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
@ -346,7 +347,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
@ -366,7 +367,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
@ -447,7 +448,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");

View file

@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.ws.rs.core.Response;
@ -84,7 +85,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB");
@ -142,7 +143,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB");
@ -204,7 +205,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
@ -280,7 +281,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A");
@ -382,7 +383,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
assertNotNull(authorization);
List<Permission> permissions = authorization.getPermissions();
Collection<Permission> permissions = authorization.getPermissions();
assertNotNull(permissions);
assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");

View file

@ -0,0 +1,184 @@
#
# * 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.
#
# This dataset provides a default realm configuration using role-based policies which can be extended to create more test scenarios:
#
# - 1 realm
# - 1 client
# - 1 user
# - 1k resources
# - Each resource with 10 scopes
# - Each resource associated with a single resource permission
# - 500 Scopes
# - 100 role policies
# - 1000 resource permissions
# - Each resource permissions associated with 10 role policies
# REALM
realms=1
realm.realm=authz-perf-tests
realm.displayName=AuthZ Performance Tests
realm.enabled=true
realm.registrationAllowed=true
realm.accessTokenLifeSpan=60
realm.passwordPolicy=hashIterations(1000)
# REALM ROLE
realmRolesPerRealm=100
realmRole.name=role_${index?string("00")}
realmRole.description=Role ${index} of ${realm.displayName}
# CLIENT
clientsPerRealm=1
client.clientId=client_${index?string("00")}
client.name=Client ${index} of ${realm.displayName}
client.description=Description of ${name}
client.rootUrl=
client.adminUrl=
client.baseUrl=http://clients.${realm.realm}.test/client_${index}
client.enabled=true
client.secret=secret
# TODO support for multiple redirect uris
#client.redirectUris=${baseUrl}/* http://load-balancing-domain.test/${clientId}/*
client.redirectUris=${baseUrl}/*
client.webOrigins=
client.protocol=openid-connect
client.publicClient=false
client.bearerOnly=false
client.authorizationServicesEnabled=true
client.serviceAccountsEnabled=true
# CLIENT ROLE
clientRolesPerClient=1
clientRole.name=clientrole_${index?string("00")}
clientRole.description=Role ${index} of ${client.name}
# USER
usersPerRealm=1
user.username=user_${index?string("00")}
user.enabled=true
user.email=${username}@email.test
user.emailVerified=true
user.firstName=User_${index}
user.lastName=O'Realm_${realm.index}
credential.type=password
credential.value=password
credential.temporary=false
# USER ATTRIBUTE
attributesPerUser=50
userAttribute.name=attribute_${index?string("00")}
userAttribute.value=<#list 0..2 as i>value_${i}_of_${name}<#sep>,</#sep></#list>
# USER ROLE MAPPINGS
realmRolesPerUser=50
clientRolesPerUser=0
# GROUP
groupsPerRealm=100
group.name=group_${index?string("00")}
# GROUP ATTRIBUTE
attributesPerGroup=50
groupAttribute.name=attribute_${index?string("00")}
groupAttribute.value=<#list 0..2 as i>value_${i}_of_${name}<#sep>,</#sep></#list>
### AUTHZ
# RESOURCE SERVER
resourceServer.allowRemoteResourceManagement=true
resourceServer.policyEnforcementMode=ENFORCING
# SCOPE
scopesPerResourceServer=10
scope.name=scope_${index}
scope.displayName=Scope ${index} of ${resourceServer.clientId}
# RESOURCE
resourcesPerResourceServer=1000
resource.name=resource_${index}
resource.displayName=Resource ${index}
resource.uri=${resourceServer.client.baseUrl}/resource_${index}
resource.type=<#if index == 0>urn:${resourceServer.clientId}:resources:default</#if>
resource.ownerManagedAccess=false
# RESOURCE MAPPINGS
scopesPerResource=10
# ROLE POLICY
rolePoliciesPerResourceServer=100
rolePolicy.name=role_policy_${index}
rolePolicy.description=Role Policy ${index} of ${resourceServer.name}
rolePolicy.logic=POSITIVE
# ROLE POLICY ROLE DEFINITION
rolePolicyRoleDefinition.required=false
realmRolesPerRolePolicy=10
clientRolesPerRolePolicy=0
# JS POLICY
jsPoliciesPerResourceServer=0
jsPolicy.name=js_policy_${index}
jsPolicy.description=JavaScript Policy ${index} of ${resourceServer.name}
jsPolicy.code=// TODO add some JavaScript code\n// for JavaScript Policy ${index}\n// more\n// lines ...
jsPolicy.logic=POSITIVE
# USER POLICY
userPoliciesPerResourceServer=0
userPolicy.name=user_policy_${index}
userPolicy.description=User Policy ${index} of ${resourceServer.name}
userPolicy.logic=POSITIVE
# USER POLICY MAPPINGS
usersPerUserPolicy=0
# CLIENT POLICY
clientPoliciesPerResourceServer=0
clientPolicy.name=client_policy_${index}
clientPolicy.description=Client Policy ${index} of ${resourceServer.name}
clientPolicy.logic=POSITIVE
# CLIENT POLICY MAPPINGS
clientsPerClientPolicy=0
# RESOURCE PERMISSION
resourcePermissionsPerResourceServer=1000
resourcePermission.name=resource_permission_${index}
resourcePermission.description=Resource Permisison ${index} of ${resourceServer.name}
resourcePermission.resourceType=<#if index == 0>urn:${resourceServer.clientId}:resources:default</#if>
resourcePermission.decisionStrategy=UNANIMOUS
# RESOURCE PERMISSION MAPPINGS
resourcesPerResourcePermission=1
policiesPerResourcePermission=10
# SCOPE PERMISSION
scopePermissionsPerResourceServer=0
scopePermission.name=scope_permission_${index}
scopePermission.description=Scope Permisison ${index} of ${resourceServer.name}
scopePermission.decisionStrategy=UNANIMOUS
# SCOPE PERMISSION MAPPINGS
scopesPerScopePermission=10
policiesPerScopePermission=5