From 80e5227bcdbea5111f6eb1424a2effa6f0aacf7a Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 27 Jul 2018 23:22:26 -0300 Subject: [PATCH] [KEYCLOAK-4902] - Refactoring and improvements to processing of authz requests --- .../authorization/AbstractPolicyEnforcer.java | 3 +- .../KeycloakAdapterPolicyEnforcer.java | 6 +- .../aggregated/AggregatePolicyProvider.java | 48 ++-- .../policy/provider/js/JSPolicyProvider.java | 15 +- .../policy/provider/js/ScriptCache.java | 30 +- .../AbstractPermissionProvider.java | 36 ++- .../permission/ResourcePolicyProvider.java | 27 ++ .../permission/ScopePolicyProvider.java | 36 +++ .../org/keycloak/AuthorizationContext.java | 4 +- .../keycloak/representations/AccessToken.java | 8 +- .../authorization/AuthorizationRequest.java | 9 + .../idm/authorization/Permission.java | 3 + .../StoreFactoryCacheManager.java | 2 +- .../StoreFactoryCacheSession.java | 71 ++++- .../jpa/store/JPAPolicyStore.java | 75 +++-- .../authorization/AuthorizationProvider.java | 30 +- .../org/keycloak/authorization/Decision.java | 4 + .../permission/evaluator/Evaluators.java | 20 +- .../IterablePermissionEvaluator.java | 42 +-- .../evaluator/PermissionEvaluator.java | 8 +- .../evaluation/AbstractDecisionCollector.java | 99 +++++++ .../DecisionPermissionCollector.java | 186 +++++++++++++ .../policy/evaluation/DecisionResult.java | 51 ---- .../evaluation/DecisionResultCollector.java | 89 +----- .../policy/evaluation/DefaultEvaluation.java | 33 ++- .../evaluation/DefaultPolicyEvaluator.java | 164 +++-------- ...ionTicketAwareDecisionResultCollector.java | 14 +- .../policy/evaluation/PolicyEvaluator.java | 6 +- .../policy/evaluation/Result.java | 79 +++--- .../authorization/store/PolicyStore.java | 7 + .../scripting/EvaluatableScriptAdapter.java | 3 + .../DefaultAuthorizationProviderFactory.java | 7 +- .../admin/PolicyEvaluationService.java | 45 ++- .../PolicyEvaluationResponseBuilder.java | 11 +- .../AuthorizationTokenService.java | 257 +++++++++++------- .../authorization/util/Permissions.java | 192 ++----------- .../oidc/endpoints/TokenEndpoint.java | 9 +- .../CompiledEvaluatableScriptAdapter.java | 10 + .../UncompiledEvaluatableScriptAdapter.java | 9 + .../admin/permissions/MgmtPermissions.java | 15 +- .../authz/AbstractResourceServerTest.java | 6 +- .../testsuite/authz/AuthorizationTest.java | 3 +- .../authz/AuthzClientCredentialsTest.java | 3 +- .../authz/ConflictingScopePermissionTest.java | 117 +++++++- .../testsuite/authz/EntitlementAPITest.java | 50 +++- .../testsuite/authz/PermissionClaimTest.java | 6 +- .../testsuite/authz/PolicyEvaluationTest.java | 2 +- .../testsuite/authz/UmaGrantTypeTest.java | 23 +- .../authz/UserManagedAccessTest.java | 11 +- .../dataset/authz/default.properties | 184 +++++++++++++ 50 files changed, 1343 insertions(+), 825 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/AbstractDecisionCollector.java create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionPermissionCollector.java delete mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResult.java create mode 100644 testsuite/performance/tests/src/test/resources/dataset/authz/default.properties diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java index bc90b65a74..2ce7a49f25 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java @@ -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 grantedPermissions = authorization.getPermissions(); + Collection grantedPermissions = authorization.getPermissions(); for (Permission permission : grantedPermissions) { if (permission.getResourceId() != null) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java index 632aa66309..e7211a9a73 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java @@ -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 grantedPermissions = authorization.getPermissions(); - List newPermissions = newAuthorization.getPermissions(); + Collection grantedPermissions = authorization.getPermissions(); + Collection newPermissions = newAuthorization.getPermissions(); for (Permission newPermission : newPermissions) { if (!grantedPermissions.contains(newPermission)) { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java index e001cef111..c647342733 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java @@ -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 results) { - if (results.isEmpty()) { - evaluation.deny(); + protected void onComplete(Result result) { + if (isGranted(result.getResults().iterator().next())) { + evaluation.grant(); } else { - Result result = results.iterator().next(); - - if (Effect.PERMIT.equals(result.getEffect())) { - evaluation.grant(); - } + evaluation.deny(); } } }; - - Policy policy = evaluation.getPolicy(); AuthorizationProvider authorization = evaluation.getAuthorizationProvider(); + Policy policy = evaluation.getPolicy(); + DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation); + Map> decisionCache = defaultEvaluation.getDecisionCache(); + ResourcePermission permission = evaluation.getPermission(); - policy.getAssociatedPolicies().forEach(associatedPolicy -> { - PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); - policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision, authorization)); - }); + for (Policy associatedPolicy : policy.getAssociatedPolicies()) { + Map 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); - decision.onComplete(); + if (effect == null) { + PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); + + policyProvider.evaluate(eval); + + eval.denyIfNoEffect(); + decisions.put(permission, eval.getEffect()); + } else { + eval.setEffect(effect); + } + } + + decision.onComplete(permission); } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java index 944ae02a90..9457836d28 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java @@ -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); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java index 4180db6915..aa0b0094ea 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java @@ -67,6 +67,12 @@ public class ScriptCache { public EvaluatableScriptAdapter computeIfAbsent(String id, Function 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; diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java index 5f200432f5..bd18379fd0 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java @@ -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 Pedro Igor */ -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> decisionCache = defaultEvaluation.getDecisionCache(); + Policy policy = evaluation.getPolicy(); + ResourcePermission permission = evaluation.getPermission(); policy.getAssociatedPolicies().forEach(associatedPolicy -> { - PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); - DefaultEvaluation.class.cast(evaluation).setPolicy(associatedPolicy); - policyProvider.evaluate(evaluation); - evaluation.denyIfNoEffect(); + Map decisions = decisionCache.computeIfAbsent(associatedPolicy, p -> new HashMap<>()); + Decision.Effect effect = decisions.get(permission); + + if (effect == null) { + PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); + defaultEvaluation.setPolicy(associatedPolicy); + policyProvider.evaluate(defaultEvaluation); + evaluation.denyIfNoEffect(); + decisions.put(permission, defaultEvaluation.getEffect()); + } else { + defaultEvaluation.setEffect(effect); + } }); } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java index 272ab2c03e..348dd8bd99 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java @@ -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 Pedro Igor */ public class ResourcePolicyProvider extends AbstractPermissionProvider { + @Override + public void evaluate(Evaluation evaluation) { + DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation); + Map> decisionCache = defaultEvaluation.getDecisionCache(); + Policy policy = defaultEvaluation.getParentPolicy(); + Map 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()); + } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java index 3f4f9d9201..930cbbca56 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java @@ -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 Pedro Igor */ public class ScopePolicyProvider extends AbstractPermissionProvider { + @Override + public void evaluate(Evaluation evaluation) { + DefaultEvaluation defaultEvaluation = DefaultEvaluation.class.cast(evaluation); + Map> decisionCache = defaultEvaluation.getDecisionCache(); + Policy policy = defaultEvaluation.getParentPolicy(); + Map 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()); + } + } + } } diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java index 0bc7b44fe8..a78bd63801 100644 --- a/core/src/main/java/org/keycloak/AuthorizationContext.java +++ b/core/src/main/java/org/keycloak/AuthorizationContext.java @@ -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 Pedro Igor @@ -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() { diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 880b9494d2..aa8fbb4156 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -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 permissions; + private Collection permissions; - public List getPermissions() { + public Collection getPermissions() { return permissions; } - public void setPermissions(List permissions) { + public void setPermissions(Collection permissions) { this.permissions = permissions; } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java index a50bf2d952..a305cf4125 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java @@ -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; + } } } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java index c1afa01954..4635613fb2 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java @@ -61,6 +61,9 @@ public class Permission { } public String getResourceId() { + if (resourceId == null || "".equals(resourceId.trim())) { + return null; + } return this.resourceId; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java index 8ee94ea5d3..9b92678de1 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java @@ -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 cache, Cache revisions) { super(cache, revisions); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index 95e1d3247f..9d075239d9 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java @@ -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 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 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 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 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 findByScopeIds(List scopeIds, String resourceServerId) { if (scopeIds == null) return null; - List result = new ArrayList<>(); + Set 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 findByScopeIds(List scopeIds, String resourceId, String resourceServerId) { if (scopeIds == null) return null; - List result = new ArrayList<>(); + Set 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 scopeIds, String resourceId, String resourceServerId, Consumer 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 List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId) { + private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId, Consumer 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 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 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()); } } } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java index 66a31d0a51..2dadfc5c13 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java @@ -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 findByResource(final String resourceId, String resourceServerId) { + List result = new LinkedList<>(); + + findByResource(resourceId, resourceServerId, result::add); + + return result; + } + + @Override + public void findByResource(String resourceId, String resourceServerId, Consumer consumer) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResource", String.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("resourceId", resourceId); query.setParameter("serverId", resourceServerId); - List result = query.getResultList(); - List 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 findByResourceType(final String resourceType, String resourceServerId) { + List result = new LinkedList<>(); + + findByResourceType(resourceType, resourceServerId, result::add); + + return result; + } + + @Override + public void findByResourceType(String resourceType, String resourceServerId, Consumer consumer) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResourceType", String.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", resourceType); query.setParameter("serverId", resourceServerId); - List result = query.getResultList(); - List 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 findByScopeIds(List scopeIds, String resourceId, String resourceServerId) { - if (scopeIds==null || scopeIds.isEmpty()) { - return Collections.emptyList(); - } + List result = new LinkedList<>(); + findByScopeIds(scopeIds, resourceId, resourceServerId, result::add); + + return result; + } + + @Override + public void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer) { // Use separate subquery to handle DB2 and MSSSQL TypedQuery query; @@ -268,15 +286,12 @@ public class JPAPolicyStore implements PolicyStore { query.setParameter("scopeIds", scopeIds); query.setParameter("serverId", resourceServerId); - List result = query.getResultList(); - List 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 diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java index 25888b8e07..4614fdd31f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -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 policyProviderFactories; private final KeycloakSession keycloakSession; private final RealmModel realm; - public AuthorizationProvider(KeycloakSession session, RealmModel realm, Map policyProviderFactories) { + public AuthorizationProvider(KeycloakSession session, RealmModel realm, Map 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 consumer) { + policyStore.findByResource(resourceId, resourceServerId, consumer); + } + @Override public List 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 scopeIds, String resourceId, String resourceServerId, Consumer consumer) { + policyStore.findByScopeIds(scopeIds, resourceId, resourceServerId, consumer); + } + @Override public List findByType(String type, String resourceServerId) { return policyStore.findByType(type, resourceServerId); @@ -404,6 +419,11 @@ public final class AuthorizationProvider implements Provider { public List findDependentPolicies(String id, String resourceServerId) { return policyStore.findDependentPolicies(id, resourceServerId); } + + @Override + public void findByResourceType(String type, String id, Consumer policyConsumer) { + policyStore.findByResourceType(type, id, policyConsumer); + } }; } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/Decision.java b/server-spi-private/src/main/java/org/keycloak/authorization/Decision.java index 6ebd086281..1fccee6937 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/Decision.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/Decision.java @@ -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 { default void onComplete() { } + + default void onComplete(ResourcePermission permission) { + } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java index 2eda4ac841..85ef479b82 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java @@ -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 permissions, EvaluationContext evaluationContext) { - return schedule(permissions, evaluationContext); - } - - public PermissionEvaluator schedule(List permissions, EvaluationContext evaluationContext) { - return new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator); + public PermissionEvaluator from(Collection permissions, EvaluationContext evaluationContext) { + return new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, authorizationProvider); } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java index c43acacb7d..959b27741a 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java @@ -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 Pedro Igor @@ -36,19 +41,24 @@ class IterablePermissionEvaluator implements PermissionEvaluator { private final Iterator permissions; private final EvaluationContext executionContext; private final PolicyEvaluator policyEvaluator; + private final AuthorizationProvider authorizationProvider; - IterablePermissionEvaluator(Iterator permissions, EvaluationContext executionContext, PolicyEvaluator policyEvaluator) { + IterablePermissionEvaluator(Iterator 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> 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 evaluate() { - AtomicReference> result = new AtomicReference<>(); + public Collection 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 results) { - result.set(results); - } - }); - - return result.get(); + return decision.results(); } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java index ae0d7fd581..196c52c508 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java @@ -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 evaluate(D decision); - List evaluate(); + Collection evaluate(ResourceServer resourceServer, AuthorizationRequest request); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/AbstractDecisionCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/AbstractDecisionCollector.java new file mode 100644 index 0000000000..151b391bea --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/AbstractDecisionCollector.java @@ -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 Pedro Igor + */ +public abstract class AbstractDecisionCollector implements Decision { + + protected final Map 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 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; + } + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionPermissionCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionPermissionCollector.java new file mode 100644 index 0000000000..701c27a7de --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionPermissionCollector.java @@ -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 Pedro Igor + */ +public class DecisionPermissionCollector extends AbstractDecisionCollector { + + private final AuthorizationProvider authorizationProvider; + private final ResourceServer resourceServer; + private final AuthorizationRequest request; + private final List 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 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 deniedScopes = new HashSet<>(); + List userManagedPermissions = new ArrayList<>(); + Collection permissionResults = new ArrayList<>(result.getResults()); + Iterator iterator = permissionResults.iterator(); + + while (iterator.hasNext()) { + Result.PolicyResult policyResult = iterator.next(); + Policy policy = policyResult.getPolicy(); + Set 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 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 results() { + return permissions; + } + + @Override + public void onError(Throwable cause) { + throw new RuntimeException("Failed to evaluate permissions", cause); + } + + protected void grantPermission(AuthorizationProvider authorizationProvider, List permissions, ResourcePermission permission, Set grantedScopes, ResourceServer resourceServer, AuthorizationRequest request, Result result) { + Set 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 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 scopes, Map> 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()); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResult.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResult.java deleted file mode 100644 index 2189c32fbf..0000000000 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResult.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class DecisionResult extends DecisionResultCollector { - protected List results; - protected Throwable error; - - @Override - protected void onComplete(List results) { - this.results = results; - - } - - @Override - public void onError(Throwable cause) { - this.error = cause; - } - - public boolean completed() { - return results != null && error == null; - } - - public List getResults() { - return results; - } - - public Throwable getError() { - return error; - } -} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java index bfba2c64d3..42609d7417 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java @@ -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 Pedro Igor */ -public abstract class DecisionResultCollector implements Decision { +public abstract class DecisionResultCollector extends AbstractDecisionCollector { - private Map 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 results); - - protected void onDeny(Result result) { - result.setStatus(Effect.DENY); - } - - private boolean isGranted(Result.PolicyResult policyResult) { - List 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; - } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java index e92dbba2d2..d46006d998 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java @@ -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> 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> 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> 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> 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(); + } + } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java index dd1a81a763..055a5a8827 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java @@ -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> 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 consumer = createDecisionConsumer(permission, executionContext, decision, verified); + Set verified = new HashSet<>(); + Consumer policyConsumer = createPolicyEvaluator(permission, authorizationProvider, executionContext, decision, verified, decisionCache); Resource resource = permission.getResource(); - List 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 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())); - } + if (!resource.getOwner().equals(resourceServer.getId())) { + for (Resource typedResource : resourceStore.findByType(resource.getType(), resourceServer.getId())) { + policyStore.findByResource(typedResource.getId(), resourceServer.getId(), policyConsumer); } - - return policies; - }, consumer); + } } } + List 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(); + if (!verified.isEmpty()) { + decision.onComplete(permission); + return; + } + + if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode)) { + DefaultEvaluation evaluation = new DefaultEvaluation(permission, executionContext, decision, authorizationProvider); + evaluation.grant(); + decision.onComplete(permission); } } - private void evaluatePolicies(Supplier> supplier, Consumer consumer) { - List policies = supplier.get(); - - if (!policies.isEmpty()) { - policies.forEach(consumer); - } - } - - private Consumer createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicBoolean verified) { - return (parentPolicy) -> { - if (!hasRequestedScopes(permission, parentPolicy)) { + private Consumer createPolicyEvaluator(ResourcePermission permission, AuthorizationProvider authorizationProvider, EvaluationContext executionContext, Decision decision, Set verified, Map> decisionCache) { + return parentPolicy -> { + if (!verified.add(parentPolicy)) { return; } - PolicyProvider policyProvider = authorization.getProvider(parentPolicy.getType()); + 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 policyResources = policy.getResources(); - - if (resourcePermission != null && !policyResources.isEmpty()) { - if (!policyResources.stream().filter(resource -> { - Iterator 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 scopes = new HashSet<>(policy.getScopes()); - - if (scopes.isEmpty()) { - Set 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 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; - } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java index 6b671e3e1c..e6a5ef9471 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java @@ -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 Pedro Igor */ -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 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 results) { - this.results = results; - } - - public List results() { - return results; - } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java index c380ba67af..7bb905e9b8 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java @@ -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; + /** *

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> decisionCache); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java index 325af3d249..95e3993d3f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java @@ -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 Pedro Igor @@ -31,33 +32,29 @@ import java.util.List; public class Result { private final ResourcePermission permission; - private List results = new ArrayList<>(); - private Effect status; + private final Map 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 getResults() { - return results; + public Collection 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 associatedPolicies = new ArrayList<>(); - private Effect status; + private final Map 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 results) { - for (PolicyResult result : results) { - if (result.getPolicy().equals(policy)) { - return result; - } - } - - PolicyResult policyResult = new PolicyResult(policy); - - results.add(policyResult); - - return policyResult; + return result; } public Policy getPolicy() { return policy; } - public List getAssociatedPolicies() { - return associatedPolicies; + public Collection 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; } } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java index 29ff235cf2..fee258b043 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java @@ -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 findByResource(String resourceId, String resourceServerId); + void findByResource(String resourceId, String resourceServerId, Consumer consumer); + /** * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given type. * @@ -121,6 +124,8 @@ public interface PolicyStore { */ List findByScopeIds(List scopeIds, String resourceId, String resourceServerId); + void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer); + /** * Returns a list of {@link Policy} with the given type. * @@ -138,4 +143,6 @@ public interface PolicyStore { * @return a list of policies that depends on the a policy with the given identifier */ List findDependentPolicies(String id, String resourceServerId); + + void findByResourceType(String type, String id, Consumer policyConsumer); } diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java index 2a76add926..2b78858d5c 100644 --- a/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java @@ -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; } diff --git a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java index d9d7b2d02c..24390b3666 100644 --- a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java +++ b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java @@ -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 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 configurePolicyProviderFactories(KeycloakSessionFactory keycloakSessionFactory) { diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java index 7aac436a77..bf80cc3dfd 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -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 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 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 permissions, ResourcePermission permission, Set 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 getResults() { + return results.values(); + } + } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java index 3842a94132..e4bf9f8671 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java @@ -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 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 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 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 { diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index 05a732f319..e9ebee39c2 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -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> 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 evaluation; + Collection 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 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 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 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 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 evaluateUserManagedPermissions(AuthorizationRequest request, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { + private Collection 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 evaluateAllPermissions(AuthorizationRequest request, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { + private Collection 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 entitlements, AuthorizationRequest request, ClientModel targetClient) { - KeycloakSession keycloakSession = getKeycloakSession(); + private AuthorizationResponse createAuthorizationResponse(KeycloakIdentity identity, Collection 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 previousPermissions = previousAuthorization.getPermissions(); + Collection 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 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 createPermissions(PermissionTicketToken ticket, AuthorizationRequest request, ResourceServer resourceServer, KeycloakIdentity identity, AuthorizationProvider authorization) { + private Collection createPermissions(PermissionTicketToken ticket, KeycloakAuthorizationRequest request, ResourceServer resourceServer, KeycloakIdentity identity, AuthorizationProvider authorization) { StoreFactory storeFactory = authorization.getStoreFactory(); Map 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 requestedScopes = requestedResource.getScopes(); + Set requestedScopes = permission.getScopes(); - if (requestedResource.getScopes() == null) { + if (permission.getScopes() == null) { requestedScopes = new HashSet<>(); } List 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 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 resources = resourceStore.findByScope(requestedScopesModel.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()); - for (Resource resource : resources) { - permissionsToEvaluate.put(resource.getId(), Permissions.createResourcePermissions(resource, requestedScopes, authorization, request)); - if (limit != null) { - limit--; + 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 permissions = authorizationData.getPermissions(); + Collection 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 permissions) { + private boolean isGranted(PermissionTicketToken ticket, AuthorizationRequest request, Collection permissions) { List 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 RealmModel getRealm() { - return getKeycloakSession().getContext().getRealm(); + 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; + } + + 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(); + } } } diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java index 903623bb56..f25dc12414 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -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 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,23 +67,41 @@ public final class Permissions { List 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 tickets = storeFactory.getPermissionTicketStore().findGranted(identity.getId(), resourceServer.getId()); - Map userManagedPermissions = new HashMap<>(); - for (PermissionTicket ticket : tickets) { - userManagedPermissions.computeIfAbsent(ticket.getResource().getId(), id -> new ResourcePermission(ticket.getResource(), new ArrayList<>(), resourceServer, request.getClaims())); + if (!tickets.isEmpty()) { + Map userManagedPermissions = new HashMap<>(); + + for (PermissionTicket ticket : tickets) { + 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()); } - 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 createResourcePermissionsWithScopes(Resource resource, List scopes, AuthorizationProvider authorization, AuthorizationRequest request) { - List permissions = new ArrayList<>(); + public static ResourcePermission createResourcePermissionsWithScopes(Resource resource, List 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 permits(List evaluation, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { - return permits(evaluation, null, authorizationProvider, resourceServer); - } - - public static List permits(List evaluation, Metadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { - Map permissions = new LinkedHashMap<>(); - - for (Result result : evaluation) { - Set deniedScopes = new HashSet<>(); - Set grantedScopes = new HashSet<>(); - boolean resourceDenied = false; - ResourcePermission permission = result.getPermission(); - List results = result.getResults(); - List userManagedPermissions = new ArrayList<>(); - int deniedCount = results.size(); - Resource resource = permission.getResource(); - - for (Result.PolicyResult policyResult : results) { - Policy policy = policyResult.getPolicy(); - Set 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 permissions, ResourcePermission permission, ResourceServer resourceServer, Metadata metadata) { - List resources = new ArrayList<>(); - Resource resource = permission.getResource(); - Set scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); - - if (resource != null) { - resources.add(resource); - } else { - List 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 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()); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 762dc558fe..adb069d24a 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -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 diff --git a/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java index 7359dc9233..be1c6d51f7 100644 --- a/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java +++ b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java @@ -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); + } + } } diff --git a/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java index 8464fdf979..4e5baee3dc 100644 --- a/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java +++ b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java @@ -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); + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java index 9b5e63c904..b0994b893d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java @@ -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 Bill Burke @@ -332,15 +330,8 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage RealmModel oldRealm = session.getContext().getRealm(); try { session.getContext().setRealm(realm); - DecisionResult decisionCollector = new DecisionResult(); - List 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); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java index 23a95ec830..00480a120c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java @@ -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 permissions, String expectedResource, String... expectedScopes) { + protected void assertPermissions(Collection permissions, String expectedResource, String... expectedScopes) { Iterator iterator = permissions.iterator(); while (iterator.hasNext()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationTest.java index 16eb0cbf7e..5418845d79 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationTest.java @@ -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) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java index 63726b50c2..7760791cc8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java @@ -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 permissions = authorization.getPermissions(); + List permissions = new ArrayList<>(authorization.getPermissions()); assertFalse(permissions.isEmpty()); assertEquals("Default Resource", permissions.get(0).getResourceName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java index 7393a94ef4..a7d4b7e6cf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java @@ -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,13 +81,14 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest { @Before public void configureAuthorization() throws Exception { - createResourcesAndScopes(); - RealmResource realm = getRealm(); ClientResource client = getClient(realm); - createPolicies(realm, client); - createPermissions(client); + if (client.authorization().resources().findByName("Resource A").isEmpty()) { + createResourcesAndScopes(); + createPolicies(realm, client); + createPermissions(client); + } } /** @@ -91,24 +97,29 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest { *

Scope Read should not be granted for Marta. */ @Test - public void testMartaCanAccessResourceAWithExecuteAndWrite() { - List 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 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 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 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 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 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); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java index 562555aea8..9287bffdd8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java @@ -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 permissions = rpt.getAuthorization().getPermissions(); + List 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 permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + Collection 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 permissions = authz.getPermissions(); + Collection permissions = authz.getPermissions(); assertNotNull(permissions); assertFalse(permissions.isEmpty()); @@ -718,7 +752,7 @@ public class EntitlementAPITest extends AbstractAuthzTest { private void assertResponse(Metadata metadata, Supplier responseSupplier) { AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getToken()).getAuthorization(); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertFalse(permissions.isEmpty()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java index 7a9600f91a..8e3e50b09b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java @@ -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 permissions = authorizationClaim.getPermissions(); + List 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 permissions = authorizationClaim.getPermissions(); + List permissions = new ArrayList<>(authorizationClaim.getPermissions()); assertEquals(1, permissions.size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java index 031ec4f62a..0b3795d6db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java @@ -664,6 +664,6 @@ public class PolicyEvaluationTest extends AbstractAuthzTest { } return baseAttributes; } - }, policy, evaluation -> {}, authorization); + }, policy, evaluation -> {}, authorization, null); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java index c89d98e95b..0d6203b521 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java @@ -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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection 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 permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB"); @@ -324,7 +325,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); @@ -346,7 +347,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); @@ -366,7 +367,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); @@ -447,7 +448,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java index 7274ca52f5..8b0ac60baa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java @@ -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 permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB"); @@ -142,7 +143,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB"); @@ -204,7 +205,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); @@ -280,7 +281,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A"); @@ -382,7 +383,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { assertNotNull(authorization); - List permissions = authorization.getPermissions(); + Collection permissions = authorization.getPermissions(); assertNotNull(permissions); assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); diff --git a/testsuite/performance/tests/src/test/resources/dataset/authz/default.properties b/testsuite/performance/tests/src/test/resources/dataset/authz/default.properties new file mode 100644 index 0000000000..8b1a05954f --- /dev/null +++ b/testsuite/performance/tests/src/test/resources/dataset/authz/default.properties @@ -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>, + +# 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>, + + +### 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 +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 +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