From 2b6597e9f14c1a737c547a7d010f5534945cb21d Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 22 May 2018 17:42:42 -0300 Subject: [PATCH] [KEYCLOAK-7367] - User-Managed Policy Provider --- .../provider/group/GroupPolicyProvider.java | 9 +- .../AbstractPermissionProvider.java | 55 ++++++++ .../ResourcePolicyProvider.java | 26 +--- .../ResourcePolicyProviderFactory.java | 18 ++- .../ScopePolicyProvider.java | 22 +-- .../ScopePolicyProviderFactory.java | 18 ++- .../permission/UMAPolicyProvider.java | 24 ++++ .../permission/UMAPolicyProviderFactory.java | 128 +++++++++++++++++ ...tion.policy.provider.PolicyProviderFactory | 7 +- .../PermissionTicketAdapter.java | 18 ++- .../authorization/PolicyAdapter.java | 14 +- .../authorization/ResourceAdapter.java | 2 +- .../StoreFactoryCacheManager.java | 7 +- .../StoreFactoryCacheSession.java | 25 +++- .../entities/CachedPermissionTicket.java | 9 ++ .../authorization/entities/CachedPolicy.java | 5 + .../events/PermissionTicketRemovedEvent.java | 6 +- .../events/PermissionTicketUpdatedEvent.java | 6 +- .../jpa/entities/PermissionTicketEntity.java | 12 ++ .../jpa/entities/PolicyEntity.java | 11 ++ .../jpa/entities/ResourceEntity.java | 2 +- .../jpa/store/JPAPermissionTicketStore.java | 54 +++++++- .../jpa/store/JPAPolicyStore.java | 10 ++ .../jpa/store/JPAResourceStore.java | 1 + .../jpa/store/PermissionTicketAdapter.java | 32 ++++- .../jpa/store/PolicyAdapter.java | 10 ++ .../jpa-changelog-authz-4.0.0.Beta3.xml | 33 +++++ .../META-INF/jpa-changelog-master.xml | 1 + .../authorization/AuthorizationProvider.java | 14 +- .../UserManagedPermissionUtil.java | 129 +++++++++++++++++ .../authorization/model/PermissionTicket.java | 5 + .../keycloak/authorization/model/Policy.java | 4 + .../policy/evaluation/DefaultEvaluation.java | 21 ++- .../evaluation/DefaultPolicyEvaluator.java | 25 ++-- .../policy/evaluation/Evaluation.java | 5 + ...ionTicketAwareDecisionResultCollector.java | 52 +------ .../provider/PolicyProviderFactory.java | 4 + .../store/PermissionTicketStore.java | 9 ++ .../models/utils/RepresentationToModel.java | 2 +- .../authorization/admin/PolicyService.java | 10 +- .../admin/ResourceSetService.java | 43 +++--- .../permission/PermissionTicketService.java | 37 ++++- .../authorization/util/Permissions.java | 29 +++- .../testsuite/authz/EntitlementAPITest.java | 131 ++++++++++++++++++ .../GroupPathWithoutGroupClaimPolicyTest.java | 109 +++++++++++++++ .../authz/PermissionManagementTest.java | 17 ++- .../testsuite/authz/PolicyEvaluationTest.java | 2 +- .../authz/UserManagedAccessTest.java | 88 +++++++++++- 48 files changed, 1125 insertions(+), 176 deletions(-) create mode 100644 authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java rename authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/{resource => permission}/ResourcePolicyProvider.java (50%) rename authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/{resource => permission}/ResourcePolicyProviderFactory.java (81%) rename authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/{scope => permission}/ScopePolicyProvider.java (53%) rename authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/{scope => permission}/ScopePolicyProviderFactory.java (69%) create mode 100644 authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java create mode 100644 authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java create mode 100755 model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.Beta3.xml create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathWithoutGroupClaimPolicyTest.java diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java index c21b4c0158..5f4fcd8da9 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java @@ -18,9 +18,12 @@ package org.keycloak.authorization.policy.provider.group; import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath; +import java.util.List; import java.util.function.Function; +import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.attribute.Attributes.Entry; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.provider.PolicyProvider; @@ -42,11 +45,13 @@ public class GroupPolicyProvider implements PolicyProvider { @Override public void evaluate(Evaluation evaluation) { GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy()); - RealmModel realm = evaluation.getAuthorizationProvider().getRealm(); + AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); + RealmModel realm = authorizationProvider.getRealm(); Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim()); if (groupsClaim == null || groupsClaim.isEmpty()) { - return; + List userGroups = evaluation.getRealm().getUserGroups(evaluation.getContext().getIdentity().getId()); + groupsClaim = new Entry(policy.getGroupsClaim(), userGroups); } for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) { 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 new file mode 100644 index 0000000000..5f200432f5 --- /dev/null +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/AbstractPermissionProvider.java @@ -0,0 +1,55 @@ +/* + * 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.provider.permission; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; +import org.keycloak.authorization.policy.evaluation.Evaluation; +import org.keycloak.authorization.policy.provider.PolicyProvider; + +/** + * @author Pedro Igor + */ +public class AbstractPermissionProvider implements PolicyProvider { + + public AbstractPermissionProvider() { + + } + + @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(); + + policy.getAssociatedPolicies().forEach(associatedPolicy -> { + PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); + DefaultEvaluation.class.cast(evaluation).setPolicy(associatedPolicy); + policyProvider.evaluate(evaluation); + evaluation.denyIfNoEffect(); + }); + } + + @Override + public void close() { + + } +} diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java similarity index 50% rename from authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java rename to authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java index 69e93fe454..272ab2c03e 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProvider.java @@ -1,13 +1,12 @@ /* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * 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 + * 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, @@ -15,26 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.authorization.policy.provider.resource; - -import org.keycloak.authorization.policy.evaluation.Evaluation; -import org.keycloak.authorization.policy.provider.PolicyProvider; +package org.keycloak.authorization.policy.provider.permission; /** * @author Pedro Igor */ -public class ResourcePolicyProvider implements PolicyProvider { +public class ResourcePolicyProvider extends AbstractPermissionProvider { - public ResourcePolicyProvider() { - - } - - @Override - public void evaluate(Evaluation evaluation) { - } - - @Override - public void close() { - - } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java similarity index 81% rename from authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java rename to authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java index 10e0ec34e4..726f584fb3 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ResourcePolicyProviderFactory.java @@ -1,4 +1,20 @@ -package org.keycloak.authorization.policy.provider.resource; +/* + * 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.provider.permission; import java.util.HashMap; import java.util.Map; diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java similarity index 53% rename from authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java rename to authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java index 0a8cf859f3..3f4f9d9201 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProvider.java @@ -1,13 +1,12 @@ /* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * 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 + * 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, @@ -15,22 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.authorization.policy.provider.scope; - -import org.keycloak.authorization.policy.evaluation.Evaluation; -import org.keycloak.authorization.policy.provider.PolicyProvider; +package org.keycloak.authorization.policy.provider.permission; /** * @author Pedro Igor */ -public class ScopePolicyProvider implements PolicyProvider { +public class ScopePolicyProvider extends AbstractPermissionProvider { - @Override - public void evaluate(Evaluation evaluation) { - } - - @Override - public void close() { - - } } diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java similarity index 69% rename from authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java rename to authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java index 70d4cceffe..7ac63da243 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/ScopePolicyProviderFactory.java @@ -1,4 +1,20 @@ -package org.keycloak.authorization.policy.provider.scope; +/* + * 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.provider.permission; import org.keycloak.Config; import org.keycloak.authorization.AuthorizationProvider; diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java new file mode 100644 index 0000000000..50f6993637 --- /dev/null +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java @@ -0,0 +1,24 @@ +/* + * 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.provider.permission; + +/** + * @author Pedro Igor + */ +public class UMAPolicyProvider extends AbstractPermissionProvider { + +} diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java new file mode 100644 index 0000000000..3d1175300e --- /dev/null +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProviderFactory.java @@ -0,0 +1,128 @@ +/* + * 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.provider.permission; + +import java.util.ArrayList; +import java.util.List; + +import org.keycloak.Config; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; + +/** + * @author Pedro Igor + */ +public class UMAPolicyProviderFactory implements PolicyProviderFactory { + + private UMAPolicyProvider provider = new UMAPolicyProvider(); + + @Override + public String getName() { + return "UMA"; + } + + @Override + public String getGroup() { + return "Others"; + } + + @Override + public boolean isInternal() { + return true; + } + + @Override + public PolicyProvider create(AuthorizationProvider authorization) { + return provider; + } + + @Override + public PolicyProvider create(KeycloakSession session) { + return null; + } + + @Override + public void onCreate(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { + verifyCircularReference(policy, new ArrayList<>()); + } + + @Override + public void onUpdate(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { + verifyCircularReference(policy, new ArrayList<>()); + } + + @Override + public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { + verifyCircularReference(policy, new ArrayList<>()); + } + + @Override + public PolicyRepresentation toRepresentation(Policy policy) { + return new PolicyRepresentation(); + } + + @Override + public Class getRepresentationType() { + return PolicyRepresentation.class; + } + + private void verifyCircularReference(Policy policy, List ids) { + if (!policy.getType().equals("uma")) { + return; + } + + if (ids.contains(policy.getId())) { + throw new RuntimeException("Circular reference found [" + policy.getName() + "]."); + } + + ids.add(policy.getId()); + + for (Policy associated : policy.getAssociatedPolicies()) { + verifyCircularReference(associated, ids); + } + } + + @Override + public void onRemove(Policy policy, AuthorizationProvider authorization) { + + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "uma"; + } +} diff --git a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory index e6fa1cc9e4..2d8c4f3f9d 100644 --- a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory +++ b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory @@ -36,10 +36,11 @@ org.keycloak.authorization.policy.provider.aggregated.AggregatePolicyProviderFactory org.keycloak.authorization.policy.provider.js.JSPolicyProviderFactory -org.keycloak.authorization.policy.provider.resource.ResourcePolicyProviderFactory +org.keycloak.authorization.policy.provider.permission.ResourcePolicyProviderFactory org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory -org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory +org.keycloak.authorization.policy.provider.permission.ScopePolicyProviderFactory org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory -org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory \ No newline at end of file +org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory +org.keycloak.authorization.policy.provider.permission.UMAPolicyProviderFactory \ No newline at end of file diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java index d6a7e0758e..3ef19cf279 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java @@ -18,6 +18,7 @@ package org.keycloak.models.cache.infinispan.authorization; import org.keycloak.authorization.model.CachedModel; 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; @@ -41,7 +42,7 @@ public class PermissionTicketAdapter implements PermissionTicket, CachedModel { getDelegateForUpdate(); cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); updated.setName(name); - } @Override @@ -278,6 +277,19 @@ public class PolicyAdapter implements Policy, CachedModel { return scopes; } + @Override + public String getOwner() { + if (isUpdated()) return updated.getOwner(); + return cached.getOwner(); + } + + @Override + public void setOwner(String owner) { + getDelegateForUpdate(); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + updated.setOwner(owner); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java index 4f59cdb11a..e6ec3d85e1 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java @@ -199,7 +199,7 @@ public class ResourceAdapter implements Resource, CachedModel { for (Scope scope : updated.getScopes()) { if (!scopes.contains(scope)) { - PermissionTicketStore permissionStore = cacheSession.getPermissionTicketStoreDelegate(); + PermissionTicketStore permissionStore = cacheSession.getPermissionTicketStore(); List permissions = permissionStore.findByScope(scope.getId(), getResourceServer().getId()); for (PermissionTicket permission : permissions) { 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 e54316d022..c3a1424b40 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 @@ -135,10 +135,11 @@ public class StoreFactoryCacheManager extends CacheManager { } } - public void permissionTicketUpdated(String id, String owner, String resource, String scope, String serverId, Set invalidations) { + public void permissionTicketUpdated(String id, String owner, String requester, String resource, String scope, String serverId, Set invalidations) { invalidations.add(id); invalidations.add(StoreFactoryCacheSession.getPermissionTicketByOwner(owner, serverId)); invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResource(resource, serverId)); + invalidations.add(StoreFactoryCacheSession.getPermissionTicketByGranted(requester, serverId)); if (scope != null) { invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(scope, serverId)); } @@ -148,8 +149,8 @@ public class StoreFactoryCacheManager extends CacheManager { policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations); } - public void permissionTicketRemoval(String id, String owner, String resource, String scope, String serverId, Set invalidations) { - permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations); + public void permissionTicketRemoval(String id, String owner, String requester, String resource, String scope, String serverId, Set invalidations) { + permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations); } } 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 5924573df6..b08629abc2 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 @@ -30,6 +30,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.jboss.logging.Logger; +import org.keycloak.authorization.UserManagedPermissionUtil; import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; @@ -283,12 +284,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, resourceTypes, scopes, serverId)); } - public void registerPermissionTicketInvalidation(String id, String owner, String resource, String scope, String serverId) { - cache.permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations); + public void registerPermissionTicketInvalidation(String id, String owner, String requester, String resource, String scope, String serverId) { + cache.permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations); PermissionTicketAdapter adapter = managedPermissionTickets.get(id); if (adapter != null) adapter.invalidateFlag(); - invalidationEvents.add(PermissionTicketUpdatedEvent.create(id, owner, resource, scope, serverId)); + invalidationEvents.add(PermissionTicketUpdatedEvent.create(id, owner, requester, resource, scope, serverId)); } private Set getResourceTypes(Set resources, String serverId) { @@ -384,6 +385,10 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { return "permission.ticket.scope." + scopeId + "." + serverId; } + public static String getPermissionTicketByGranted(String userId, String serverId) { + return "permission.ticket.granted." + userId + "." + serverId; + } + public static String getPermissionTicketByOwner(String owner, String serverId) { return "permission.ticket.owner." + owner + "." + serverId; } @@ -836,7 +841,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { @Override public PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer) { PermissionTicket created = getPermissionTicketStoreDelegate().create(resourceId, scopeId, requester, resourceServer); - registerPermissionTicketInvalidation(created.getId(), created.getOwner(), created.getResource().getId(), scopeId, created.getResourceServer().getId()); + registerPermissionTicketInvalidation(created.getId(), created.getOwner(), created.getRequester(), created.getResource().getId(), scopeId, created.getResourceServer().getId()); return created; } @@ -851,9 +856,10 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { if (permission.getScope() != null) { scopeId = permission.getScope().getId(); } - invalidationEvents.add(PermissionTicketRemovedEvent.create(id, permission.getOwner(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId())); - cache.permissionTicketRemoval(id, permission.getOwner(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId(), invalidations); + invalidationEvents.add(PermissionTicketRemovedEvent.create(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId())); + cache.permissionTicketRemoval(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId(), invalidations); getPermissionTicketStoreDelegate().delete(id); + UserManagedPermissionUtil.removePolicy(permission, StoreFactoryCacheSession.this); } @@ -908,6 +914,13 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { return getPermissionTicketStoreDelegate().find(attributes, resourceServerId, firstResult, maxResult); } + @Override + public List findGranted(String userId, String resourceServerId) { + String cacheKey = getPermissionTicketByGranted(userId, resourceServerId); + return cacheQuery(cacheKey, PermissionTicketListQuery.class, () -> getPermissionTicketStoreDelegate().findGranted(userId, resourceServerId), + (revision, permissions) -> new PermissionTicketListQuery(revision, cacheKey, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId); + } + @Override public List findByOwner(String owner, String resourceServerId) { String cacheKey = getPermissionTicketByOwner(owner, resourceServerId); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java index a906a7d465..40631b29ca 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java @@ -18,6 +18,7 @@ package org.keycloak.models.cache.infinispan.authorization.entities; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; /** @@ -33,6 +34,7 @@ public class CachedPermissionTicket extends AbstractRevisioned implements InReso private boolean granted; private Long createdTimestamp; private Long grantedTimestamp; + private String policy; public CachedPermissionTicket(Long revision, PermissionTicket permissionTicket) { super(revision, permissionTicket.getId()); @@ -46,6 +48,10 @@ public class CachedPermissionTicket extends AbstractRevisioned implements InReso this.granted = permissionTicket.isGranted(); createdTimestamp = permissionTicket.getCreatedTimestamp(); grantedTimestamp = permissionTicket.getGrantedTimestamp(); + Policy policy = permissionTicket.getPolicy(); + if (policy != null) { + this.policy = policy.getId(); + } } public String getOwner() { @@ -80,4 +86,7 @@ public class CachedPermissionTicket extends AbstractRevisioned implements InReso return this.resourceServerId; } + public String getPolicy() { + return policy; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java index 11d6e64ab2..643c8683ee 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java @@ -45,6 +45,7 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer private Set associatedPoliciesIds; private Set resourcesIds; private Set scopesIds; + private final String owner; public CachedPolicy(Long revision, Policy policy) { super(revision, policy.getId()); @@ -58,6 +59,7 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer this.associatedPoliciesIds = policy.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet()); this.resourcesIds = policy.getResources().stream().map(Resource::getId).collect(Collectors.toSet()); this.scopesIds = policy.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + this.owner = policy.getOwner(); } public String getType() { @@ -100,4 +102,7 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer return this.resourceServerId; } + public String getOwner() { + return owner; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java index bbef9799d6..f5296e6ab2 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java @@ -32,11 +32,13 @@ public class PermissionTicketRemovedEvent extends InvalidationEvent implements A private String resource; private String scope; private String serverId; + private String requester; - public static PermissionTicketRemovedEvent create(String id, String owner, String resource, String scope, String serverId) { + public static PermissionTicketRemovedEvent create(String id, String owner, String requester, String resource, String scope, String serverId) { PermissionTicketRemovedEvent event = new PermissionTicketRemovedEvent(); event.id = id; event.owner = owner; + event.requester = requester; event.resource = resource; event.scope = scope; event.serverId = serverId; @@ -55,6 +57,6 @@ public class PermissionTicketRemovedEvent extends InvalidationEvent implements A @Override public void addInvalidations(StoreFactoryCacheManager cache, Set invalidations) { - cache.permissionTicketRemoval(id, owner, resource, scope, serverId, invalidations); + cache.permissionTicketRemoval(id, owner, requester, resource, scope, serverId, invalidations); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java index 1d830edd0c..332353104a 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java @@ -32,11 +32,13 @@ public class PermissionTicketUpdatedEvent extends InvalidationEvent implements A private String resource; private String scope; private String serverId; + private String requester; - public static PermissionTicketUpdatedEvent create(String id, String owner, String resource, String scope, String serverId) { + public static PermissionTicketUpdatedEvent create(String id, String owner, String requester, String resource, String scope, String serverId) { PermissionTicketUpdatedEvent event = new PermissionTicketUpdatedEvent(); event.id = id; event.owner = owner; + event.requester = requester; event.resource = resource; event.scope = scope; event.serverId = serverId; @@ -55,6 +57,6 @@ public class PermissionTicketUpdatedEvent extends InvalidationEvent implements A @Override public void addInvalidations(StoreFactoryCacheManager cache, Set invalidations) { - cache.permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations); + cache.permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations); } } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java index 1cd8d07780..4bf334c951 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java @@ -76,6 +76,10 @@ public class PermissionTicketEntity { @JoinColumn(name = "RESOURCE_SERVER_ID") private ResourceServerEntity resourceServer; + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "POLICY_ID") + private PolicyEntity policy; + public String getId() { return id; } @@ -144,6 +148,14 @@ public class PermissionTicketEntity { return grantedTimestamp != null; } + public PolicyEntity getPolicy() { + return policy; + } + + public void setPolicy(PolicyEntity policy) { + this.policy = policy; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java index 984c6ba092..e235f586d5 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java @@ -113,6 +113,9 @@ public class PolicyEntity { @JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID")) private Set scopes = new HashSet<>(); + @Column(name = "OWNER") + private String owner; + public String getId() { return this.id; } @@ -201,6 +204,14 @@ public class PolicyEntity { this.associatedPolicies = associatedPolicies; } + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java index 8c9960bbb5..8b1d9033db 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java @@ -56,7 +56,7 @@ import org.hibernate.annotations.FetchMode; @NamedQuery(name="findAnyResourceIdByOwner", query="select r.id from ResourceEntity r where r.owner = :owner"), @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.uri = :uri"), @NamedQuery(name="findResourceIdByName", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :ownerId and r.name = :name"), - @NamedQuery(name="findResourceIdByType", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.type = :type"), + @NamedQuery(name="findResourceIdByType", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :ownerId and r.type = :type"), @NamedQuery(name="findResourceIdByServerId", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId "), @NamedQuery(name="findResourceIdByScope", query="select r.id from ResourceEntity r inner join r.scopes s where r.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id in (:scopeIds))"), @NamedQuery(name="deleteResourceByResourceServer", query="delete from ResourceEntity r where r.resourceServer.id = :serverId") diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java index ee51d5e124..3ab4da99a3 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java @@ -18,9 +18,11 @@ package org.keycloak.authorization.jpa.store; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.persistence.EntityManager; import javax.persistence.FlushModeType; @@ -102,9 +104,15 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { List result = query.getResultList(); List list = new LinkedList<>(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + PermissionTicket ticket = ticketStore.findById(id, resourceServerId); + if (Objects.nonNull(ticket)) { + list.add(ticket); + } } + return list; } @@ -118,9 +126,15 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { List result = query.getResultList(); List list = new LinkedList<>(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + PermissionTicket ticket = ticketStore.findById(id, resourceServerId); + if (Objects.nonNull(ticket)) { + list.add(ticket); + } } + return list; } @@ -139,9 +153,15 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { List result = query.getResultList(); List list = new LinkedList<>(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + PermissionTicket ticket = ticketStore.findById(id, resourceServerId); + if (Objects.nonNull(ticket)) { + list.add(ticket); + } } + return list; } @@ -184,6 +204,8 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { } } else if (PermissionTicket.REQUESTER_IS_NULL.equals(name)) { predicates.add(builder.isNull(root.get("requester"))); + } else if (PermissionTicket.POLICY_IS_NOT_NULL.equals(name)) { + predicates.add(builder.isNotNull(root.get("policy"))); } else { throw new RuntimeException("Unsupported filter [" + name + "]"); } @@ -196,21 +218,35 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { if (firstResult != -1) { query.setFirstResult(firstResult); } + if (maxResult != -1) { query.setMaxResults(maxResult); } List result = query.getResultList(); List list = new LinkedList<>(); - PermissionTicketStore ticket = provider.getStoreFactory().getPermissionTicketStore(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); for (String id : result) { - list.add(ticket.findById(id, resourceServerId)); + PermissionTicket ticket = ticketStore.findById(id, resourceServerId); + if (Objects.nonNull(ticket)) { + list.add(ticket); + } } return list; } + @Override + public List findGranted(String userId, String resourceServerId) { + HashMap filters = new HashMap<>(); + + filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.REQUESTER, userId); + + return find(filters, resourceServerId, -1, -1); + } + @Override public List findByOwner(String owner, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByType", String.class); @@ -221,9 +257,15 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { List result = query.getResultList(); List list = new LinkedList<>(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + PermissionTicket ticket = ticketStore.findById(id, resourceServerId); + if (Objects.nonNull(ticket)) { + list.add(ticket); + } } + return list; } } 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 139973ccf7..021f451347 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 @@ -142,11 +142,21 @@ public class JPAPolicyStore implements PolicyStore { } } else if ("id".equals(name)) { predicates.add(root.get(name).in(value)); + } else if ("owner".equals(name)) { + predicates.add(root.get(name).in(value)); + } else if ("owner_is_not_null".equals(name)) { + predicates.add(builder.isNotNull(root.get("owner"))); + } else if ("resource".equals(name)) { + predicates.add(root.join("resources").get("id").in(value)); } else { predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); } }); + if (!attributes.containsKey("owner") && !attributes.containsKey("owner_is_not_null")) { + predicates.add(builder.isNull(root.get("owner"))); + } + querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("name"))); Query query = entityManager.createQuery(querybuilder); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java index b9cecf7056..7f6338d506 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java @@ -267,6 +267,7 @@ public class JPAResourceStore implements ResourceStore { query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", type); + query.setParameter("ownerId", resourceServerId); query.setParameter("serverId", resourceServerId); List result = query.getResultList(); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java index e1c56a2593..6f66a2e291 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java @@ -16,9 +16,12 @@ */ package org.keycloak.authorization.jpa.store; +import static org.keycloak.authorization.UserManagedPermissionUtil.updatePolicy; + import javax.persistence.EntityManager; import org.keycloak.authorization.jpa.entities.PermissionTicketEntity; +import org.keycloak.authorization.jpa.entities.PolicyEntity; import org.keycloak.authorization.jpa.entities.ScopeEntity; import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.Policy; @@ -34,13 +37,13 @@ import org.keycloak.models.jpa.JpaModel; */ public class PermissionTicketAdapter implements PermissionTicket, JpaModel { - private PermissionTicketEntity entity; - private EntityManager em; - private StoreFactory storeFactory; + private final EntityManager entityManager; + private final PermissionTicketEntity entity; + private final StoreFactory storeFactory; - public PermissionTicketAdapter(PermissionTicketEntity entity, EntityManager em, StoreFactory storeFactory) { + public PermissionTicketAdapter(PermissionTicketEntity entity, EntityManager entityManager, StoreFactory storeFactory) { this.entity = entity; - this.em = em; + this.entityManager = entityManager; this.storeFactory = storeFactory; } @@ -82,6 +85,7 @@ public class PermissionTicketAdapter implements PermissionTicket, JpaModel { entity.getResources().remove(ResourceAdapter.toEntity(em, resource)); } + @Override + public void setOwner(String owner) { + entity.setOwner(owner); + } + + @Override + public String getOwner() { + return entity.getOwner(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.Beta3.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.Beta3.xml new file mode 100755 index 0000000000..e5073da4ba --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.Beta3.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index c9d0e166cd..554df64363 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -55,4 +55,5 @@ + 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 88c898b3ce..57f4c9373c 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 @@ -235,7 +235,7 @@ public final class AuthorizationProvider implements Provider { @Override public void delete(String id) { Scope scope = findById(id, null); - PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore(); + PermissionTicketStore ticketStore = AuthorizationProvider.this.getStoreFactory().getPermissionTicketStore(); List permissions = ticketStore.findByScope(id, scope.getResourceServer().getId()); for (PermissionTicket permission : permissions) { @@ -414,6 +414,7 @@ public final class AuthorizationProvider implements Provider { @Override public void delete(String id) { Resource resource = findById(id, null); + StoreFactory storeFactory = AuthorizationProvider.this.getStoreFactory(); PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore(); List permissions = ticketStore.findByResource(id, resource.getResourceServer().getId()); @@ -421,6 +422,17 @@ public final class AuthorizationProvider implements Provider { ticketStore.delete(permission.getId()); } + PolicyStore policyStore = storeFactory.getPolicyStore(); + List policies = policyStore.findByResource(id, resource.getResourceServer().getId()); + + for (Policy policyModel : policies) { + if (policyModel.getResources().size() == 1) { + policyStore.delete(policyModel.getId()); + } else { + policyModel.removeResource(resource); + } + } + delegate.delete(id); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java b/server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java new file mode 100644 index 0000000000..711d4d7dc9 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java @@ -0,0 +1,129 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.List; + +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; + +/** + * @author Pedro Igor + */ +public class UserManagedPermissionUtil { + + public static void updatePolicy(PermissionTicket ticket, StoreFactory storeFactory) { + Scope scope = ticket.getScope(); + Policy policy = ticket.getPolicy(); + + if (policy == null) { + HashMap filter = new HashMap<>(); + + filter.put(PermissionTicket.OWNER, ticket.getOwner()); + filter.put(PermissionTicket.REQUESTER, ticket.getRequester()); + filter.put(PermissionTicket.RESOURCE, ticket.getResource().getId()); + filter.put(PermissionTicket.POLICY_IS_NOT_NULL, Boolean.TRUE.toString()); + + List tickets = storeFactory.getPermissionTicketStore().find(filter, ticket.getResourceServer().getId(), -1, 1); + + if (!tickets.isEmpty()) { + policy = tickets.iterator().next().getPolicy(); + } + } + + if (ticket.isGranted()) { + if (policy == null) { + policy = createUserManagedPermission(ticket, storeFactory); + } + + if (scope != null && !policy.getScopes().contains(scope)) { + policy.addScope(scope); + } + + ticket.setPolicy(policy); + } else if (scope != null) { + policy.removeScope(scope); + ticket.setPolicy(null); + } + } + + public static void removePolicy(PermissionTicket ticket, StoreFactory storeFactory) { + Policy policy = ticket.getPolicy(); + + if (policy != null) { + HashMap filter = new HashMap<>(); + + filter.put(PermissionTicket.OWNER, ticket.getOwner()); + filter.put(PermissionTicket.REQUESTER, ticket.getRequester()); + filter.put(PermissionTicket.RESOURCE, ticket.getResource().getId()); + filter.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + + List tickets = storeFactory.getPermissionTicketStore().find(filter, ticket.getResourceServer().getId(), -1, -1); + + if (tickets.isEmpty()) { + PolicyStore policyStore = storeFactory.getPolicyStore(); + + for (Policy associatedPolicy : policy.getAssociatedPolicies()) { + policyStore.delete(associatedPolicy.getId()); + } + + policyStore.delete(policy.getId()); + } else if (ticket.getScope() != null) { + policy.removeScope(ticket.getScope()); + } + } + } + + private static Policy createUserManagedPermission(PermissionTicket ticket, StoreFactory storeFactory) { + PolicyStore policyStore = storeFactory.getPolicyStore(); + UserPolicyRepresentation userPolicyRep = new UserPolicyRepresentation(); + + userPolicyRep.setName(KeycloakModelUtils.generateId()); + userPolicyRep.addUser(ticket.getRequester()); + + Policy userPolicy = policyStore.create(userPolicyRep, ticket.getResourceServer()); + + userPolicy.setOwner(ticket.getOwner()); + + PolicyRepresentation policyRep = new PolicyRepresentation(); + + policyRep.setName(KeycloakModelUtils.generateId()); + policyRep.setType("uma"); + policyRep.addPolicy(userPolicy.getId()); + + Policy policy = policyStore.create(policyRep, ticket.getResourceServer()); + + policy.setOwner(ticket.getOwner()); + policy.addResource(ticket.getResource()); + + Scope scope = ticket.getScope(); + + if (scope != null) { + policy.addScope(scope); + } + + return policy; + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java index 39366d6de5..493bfc278a 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java @@ -29,6 +29,7 @@ public interface PermissionTicket { String GRANTED = "granted"; String REQUESTER = "requester"; String REQUESTER_IS_NULL = "requester_is_null"; + String POLICY_IS_NOT_NULL = "policy_is_not_null"; /** * Returns the unique identifier for this instance. @@ -73,4 +74,8 @@ public interface PermissionTicket { * @return a resource server */ ResourceServer getResourceServer(); + + Policy getPolicy(); + + void setPolicy(Policy policy); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Policy.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Policy.java index f46f61ad29..0dafea9217 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Policy.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Policy.java @@ -148,6 +148,10 @@ public interface Policy { */ Set getScopes(); + String getOwner(); + + void setOwner(String owner); + void addScope(Scope scope); void removeScope(Scope scope); 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 cbf23cb11b..e92dbba2d2 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 @@ -48,12 +48,21 @@ public class DefaultEvaluation implements Evaluation { private final ResourcePermission permission; private final EvaluationContext executionContext; private final Decision decision; - private final Policy policy; + private Policy policy; private final Policy parentPolicy; private final AuthorizationProvider authorizationProvider; 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, Policy policy, Decision decision, AuthorizationProvider authorizationProvider) { this.permission = permission; this.executionContext = executionContext; @@ -98,6 +107,9 @@ public class DefaultEvaluation implements Evaluation { @Override public Policy getPolicy() { + if (policy == null) { + return parentPolicy; + } return this.policy; } @@ -119,7 +131,8 @@ public class DefaultEvaluation implements Evaluation { return effect; } - void denyIfNoEffect() { + @Override + public void denyIfNoEffect() { if (this.effect == null) { deny(); } @@ -248,4 +261,8 @@ public class DefaultEvaluation implements Evaluation { } }; } + + public void setPolicy(Policy policy) { + this.policy = policy; + } } 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 0cc0622211..24127d6405 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 @@ -63,7 +63,7 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator { PolicyEnforcementMode enforcementMode = resourceServer.getPolicyEnforcementMode(); if (PolicyEnforcementMode.DISABLED.equals(enforcementMode)) { - createEvaluation(permission, executionContext, decision, null, null).grant(); + createEvaluation(permission, executionContext, decision, null).grant(); return; } @@ -95,7 +95,7 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator { } if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode) && !verified.get()) { - createEvaluation(permission, executionContext, decision, null, null).grant(); + createEvaluation(permission, executionContext, decision, null).grant(); } } @@ -113,25 +113,22 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator { return; } - for (Policy associatedPolicy : parentPolicy.getAssociatedPolicies()) { - PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType()); + PolicyProvider policyProvider = authorization.getProvider(parentPolicy.getType()); - if (policyProvider == null) { - throw new RuntimeException("Unknown parentPolicy provider for type [" + associatedPolicy.getType() + "]."); - } - - DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy, associatedPolicy); - - policyProvider.evaluate(evaluation); - evaluation.denyIfNoEffect(); + 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); }; } - private DefaultEvaluation createEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, Policy parentPolicy, Policy associatedPolicy) { - return new DefaultEvaluation(permission, executionContext, parentPolicy, associatedPolicy, decision, authorization); + 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) { diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java index 7fd656655b..cdc0017be1 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java @@ -69,4 +69,9 @@ public interface Evaluation { * Denies the requested permission. */ void deny(); + + /** + * Denies the requested permission if a decision was not made yet. + */ + void denyIfNoEffect(); } 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 df7c705eb6..d86888de79 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 @@ -57,53 +57,6 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult this.authorization = authorization; } - @Override - protected void onDeny(Result result) { - ResourcePermission permission = result.getPermission(); - Resource resource = permission.getResource(); - - if (resource != null && resource.isOwnerManagedAccess()) { - if (!resource.getOwner().equals(identity.getId())) { - Map filters = new HashMap<>(); - - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.REQUESTER, identity.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - - List permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); - - if (!permissions.isEmpty()) { - List grantedScopes = new ArrayList<>(); - - for (PolicyResult policyResult : result.getResults()) { - for (PermissionTicket ticket : permissions) { - Scope grantedScope = ticket.getScope(); - - if ("resource".equals(policyResult.getPolicy().getType())) { - policyResult.setStatus(Effect.PERMIT); - } - - if (grantedScope != null) { - grantedScopes.add(grantedScope); - - for (Scope policyScope : policyResult.getPolicy().getScopes()) { - if (policyScope.equals(grantedScope)) { - policyResult.setStatus(Effect.PERMIT); - } - } - } - } - } - - permission.getScopes().clear(); - permission.getScopes().addAll(grantedScopes); - } - } - } - - super.onDeny(result); - } - @Override public void onComplete() { super.onComplete(); @@ -111,9 +64,10 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult if (request.isSubmitRequest()) { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); + List resources = ticket.getResources(); - if (ticket.getResources() != null) { - for (PermissionTicketToken.ResourcePermission permission : ticket.getResources()) { + if (resources != null) { + for (PermissionTicketToken.ResourcePermission permission : resources) { Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId()); if (resource == null) { diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java index d795d8ab6c..1d353fdcd2 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java @@ -34,6 +34,10 @@ public interface PolicyProviderFactory e String getGroup(); + default boolean isInternal() { + return false; + } + PolicyProvider create(AuthorizationProvider authorization); R toRepresentation(Policy policy); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java index 654d68b727..a561712337 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java @@ -90,4 +90,13 @@ public interface PermissionTicketStore { List findByScope(String scopeId, String resourceServerId); List find(Map attributes, String resourceServerId, int firstResult, int maxResult); + + /** + * Returns a list of {@link PermissionTicket} granted to the given {@code userId}. + * + * @param userId the user id + * @param resourceServerId the resource server id + * @return a list of permissions granted for a particular user + */ + List findGranted(String userId, String resourceServerId); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 1deada1454..fe449cd794 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -2444,7 +2444,7 @@ public class RepresentationToModel { if (granted && !ticket.isGranted()) { ticket.setGrantedTimestamp(System.currentTimeMillis()); } else if (!granted) { - ticket.setGrantedTimestamp(null); + ticketStore.delete(ticket.getId()); } return ticket; diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java index e835ec639c..cb87631581 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -55,7 +55,6 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.AdminEventBuilder; @@ -253,12 +252,13 @@ public class PolicyService { this.auth.realm().requireViewAuthorization(); return Response.ok( authorization.getProviderFactories().stream() - .map(provider -> { + .filter(factory -> !factory.isInternal()) + .map(factory -> { PolicyProviderRepresentation representation = new PolicyProviderRepresentation(); - representation.setName(provider.getName()); - representation.setGroup(provider.getGroup()); - representation.setType(provider.getId()); + representation.setName(factory.getName()); + representation.setGroup(factory.getGroup()); + representation.setType(factory.getId()); return representation; }) diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java index edde9ccd36..5e2ba9507a 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java @@ -164,17 +164,6 @@ public class ResourceSetService { return Response.status(Status.NOT_FOUND).build(); } - PolicyStore policyStore = storeFactory.getPolicyStore(); - List policies = policyStore.findByResource(id, resourceServer.getId()); - - for (Policy policyModel : policies) { - if (policyModel.getResources().size() == 1) { - policyStore.delete(policyModel.getId()); - } else { - policyModel.removeResource(resource); - } - } - storeFactory.getResourceStore().delete(id); if (authorization.getRealm().isAdminEventsEnabled()) { @@ -254,7 +243,8 @@ public class ResourceSetService { public Response getPermissions(@PathParam("id") String id) { requireView(); StoreFactory storeFactory = authorization.getStoreFactory(); - Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId()); + ResourceStore resourceStore = storeFactory.getResourceStore(); + Resource model = resourceStore.findById(id, resourceServer.getId()); if (model == null) { return Response.status(Status.NOT_FOUND).build(); @@ -264,21 +254,36 @@ public class ResourceSetService { Set policies = new HashSet<>(); policies.addAll(policyStore.findByResource(model.getId(), resourceServer.getId())); - policies.addAll(policyStore.findByResourceType(model.getType(), resourceServer.getId())); + + if (model.getType() != null) { + policies.addAll(policyStore.findByResourceType(model.getType(), resourceServer.getId())); + + HashMap resourceFilter = new HashMap<>(); + + resourceFilter.put("owner", new String[]{resourceServer.getId()}); + resourceFilter.put("type", new String[]{model.getType()}); + + for (Resource resourceType : resourceStore.findByResourceServer(resourceFilter, resourceServer.getId(), -1, -1)) { + policies.addAll(policyStore.findByResource(resourceType.getId(), resourceServer.getId())); + } + } + policies.addAll(policyStore.findByScopeIds(model.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), id, resourceServer.getId())); policies.addAll(policyStore.findByScopeIds(model.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), null, resourceServer.getId())); List representation = new ArrayList<>(); for (Policy policyModel : policies) { - PolicyRepresentation policy = new PolicyRepresentation(); + if (!"uma".equalsIgnoreCase(policyModel.getType())) { + PolicyRepresentation policy = new PolicyRepresentation(); - policy.setId(policyModel.getId()); - policy.setName(policyModel.getName()); - policy.setType(policyModel.getType()); + policy.setId(policyModel.getId()); + policy.setName(policyModel.getName()); + policy.setType(policyModel.getType()); - if (!representation.contains(policy)) { - representation.add(policy); + if (!representation.contains(policy)) { + representation.add(policy); + } } } diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java index 951a780dc9..59ff1fddb8 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java @@ -23,7 +23,10 @@ import org.keycloak.authorization.common.KeycloakIdentity; import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.Constants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserProvider; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; @@ -182,7 +185,8 @@ public class PermissionTicketService { @QueryParam("returnNames") Boolean returnNames, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResult) { - PermissionTicketStore permissionTicketStore = authorization.getStoreFactory().getPermissionTicketStore(); + StoreFactory storeFactory = authorization.getStoreFactory(); + PermissionTicketStore permissionTicketStore = storeFactory.getPermissionTicketStore(); Map filters = new HashMap<>(); @@ -191,15 +195,22 @@ public class PermissionTicketService { } if (scopeId != null) { - filters.put(PermissionTicket.SCOPE, scopeId); + ScopeStore scopeStore = storeFactory.getScopeStore(); + Scope scope = scopeStore.findById(scopeId, resourceServer.getId()); + + if (scope == null) { + scope = scopeStore.findByName(scopeId, resourceServer.getId()); + } + + filters.put(PermissionTicket.SCOPE, scope != null ? scope.getId() : scopeId); } if (owner != null) { - filters.put(PermissionTicket.OWNER, owner); + filters.put(PermissionTicket.OWNER, getUserId(owner)); } if (requester != null) { - filters.put(PermissionTicket.REQUESTER, requester); + filters.put(PermissionTicket.REQUESTER, getUserId(requester)); } if (granted != null) { @@ -212,4 +223,22 @@ public class PermissionTicketService { .collect(Collectors.toList())) .build(); } + + private String getUserId(String userIdOrName) { + UserProvider userProvider = authorization.getKeycloakSession().users(); + RealmModel realm = authorization.getRealm(); + UserModel userModel = userProvider.getUserById(userIdOrName, realm); + + if (userModel != null) { + return userModel.getId(); + } + + userModel = userProvider.getUserByUsername(userIdOrName, realm); + + if (userModel != null) { + return userModel.getId(); + } + + return userIdOrName; + } } 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 38b65b8ac3..180ec33e04 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -20,6 +20,7 @@ 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; @@ -33,6 +34,7 @@ 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; @@ -71,9 +73,22 @@ public final class Permissions { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); + // 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))); + + // 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))); + // 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)); + } + + permissions.addAll(userManagedPermissions.values()); + return permissions; } @@ -156,7 +171,9 @@ public final class Permissions { 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(); @@ -175,6 +192,8 @@ public final class Permissions { // 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.isOwnerManagedAccess() && "uma".equals(policy.getType())) { + userManagedPermissions.add(policyResult); } deniedCount--; } else { @@ -183,7 +202,7 @@ public final class Permissions { deniedScopes.addAll(policyScopes); } else if (isResourcePermission(policy)) { resourceDenied = true; - deniedScopes.addAll(permission.getResource().getScopes()); + deniedScopes.addAll(resource.getScopes()); } } } @@ -193,6 +212,14 @@ public final class Permissions { 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()) { 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 167761fbc4..60bc6f8a29 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 @@ -23,7 +23,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.function.Supplier; @@ -40,12 +42,16 @@ import org.keycloak.authorization.client.Configuration; import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.common.util.Base64Url; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Authorization; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; import org.keycloak.representations.idm.authorization.AuthorizationResponse; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.Permission; +import org.keycloak.representations.idm.authorization.PermissionRequest; +import org.keycloak.representations.idm.authorization.PermissionResponse; +import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.testsuite.util.ClientBuilder; @@ -390,6 +396,108 @@ public class EntitlementAPITest extends AbstractAuthzTest { PAIRWISE_AUTHZ_CLIENT_CONFIG); } + @Test + public void testObtainAllEntitlements() throws Exception { + ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST); + AuthorizationResource authorization = client.authorization(); + + JSPolicyRepresentation policy = new JSPolicyRepresentation(); + + policy.setName("Only Owner Policy"); + policy.setCode("if ($evaluation.getContext().getIdentity().getId() == $evaluation.getPermission().getResource().getOwner()) {$evaluation.grant();}"); + + authorization.policies().js().create(policy).close(); + + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Marta Resource"); + resource.setOwner("marta"); + resource.setOwnerManagedAccess(true); + + resource = authorization.resources().create(resource).readEntity(ResourceRepresentation.class); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName("Marta Resource Permission"); + permission.addResource(resource.getId()); + permission.addPolicy(policy.getName()); + + authorization.permissions().resource().create(permission); + + assertTrue(hasPermission("marta", "password", resource.getId())); + assertFalse(hasPermission("kolo", "password", resource.getId())); + + String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken(); + AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG); + PermissionResponse permissionResponse = authzClient.protection().permission().create(new PermissionRequest(resource.getId())); + AuthorizationRequest request = new AuthorizationRequest(); + + request.setTicket(permissionResponse.getTicket()); + + try { + authzClient.authorization(accessToken).authorize(request); + } catch (Exception ignore) { + + } + + List tickets = authzClient.protection().permission().findByResource(resource.getId()); + + assertEquals(1, tickets.size()); + + PermissionTicketRepresentation ticket = tickets.get(0); + + ticket.setGranted(true); + + authzClient.protection().permission().update(ticket); + + assertTrue(hasPermission("kolo", "password", resource.getId())); + + resource.addScope("Scope A"); + + authorization.resources().resource(resource.getId()).update(resource); + + // the addition of a new scope invalidates the permission previously grante to the resource + assertFalse(hasPermission("kolo", "password", resource.getId())); + + accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken(); + permissionResponse = authzClient.protection().permission().create(new PermissionRequest(resource.getId(), "Scope A")); + request = new AuthorizationRequest(); + + request.setTicket(permissionResponse.getTicket()); + + try { + authzClient.authorization(accessToken).authorize(request); + } catch (Exception ignore) { + + } + + tickets = authzClient.protection().permission().find(resource.getId(), "Scope A", null, null, false, false, null, null); + + assertEquals(1, tickets.size()); + + ticket = tickets.get(0); + + ticket.setGranted(true); + + authzClient.protection().permission().update(ticket); + + assertTrue(hasPermission("kolo", "password", resource.getId(), "Scope A")); + + resource.addScope("Scope B"); + + authorization.resources().resource(resource.getId()).update(resource); + + assertTrue(hasPermission("kolo", "password", resource.getId())); + assertFalse(hasPermission("kolo", "password", resource.getId(), "Scope B")); + + resource.setScopes(new HashSet<>()); + + authorization.resources().resource(resource.getId()).update(resource); + + assertTrue(hasPermission("kolo", "password", resource.getId())); + assertFalse(hasPermission("kolo", "password", resource.getId(), "Scope A")); + } + public void testResourceServerAsAudience(String testClientId, String resourceServerClientId, String configFile) throws Exception { AuthorizationRequest request = new AuthorizationRequest(); @@ -402,6 +510,29 @@ public class EntitlementAPITest extends AbstractAuthzTest { assertEquals(resourceServerClientId, rpt.getAudience()[0]); } + private boolean hasPermission(String userName, String password, String resourceId, String... scopeIds) throws Exception { + String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", userName, password).getAccessToken(); + AuthorizationResponse response = getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization(accessToken).authorize(new AuthorizationRequest()); + AccessToken rpt = toAccessToken(response.getToken()); + Authorization authz = rpt.getAuthorization(); + List permissions = authz.getPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + + for (Permission grantedPermission : permissions) { + if (grantedPermission.getResourceId().equals(resourceId)) { + return scopeIds == null || scopeIds.length == 0 || grantedPermission.getScopes().containsAll(Arrays.asList(scopeIds)); + } + } + + return false; + } + + private boolean hasPermission(String userName, String password, String resourceId) throws Exception { + return hasPermission(userName, password, resourceId, null); + } + private void assertResponse(Metadata metadata, Supplier responseSupplier) { AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getToken()).getAuthorization(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathWithoutGroupClaimPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathWithoutGroupClaimPolicyTest.java new file mode 100644 index 0000000000..3936308e42 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathWithoutGroupClaimPolicyTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.authz; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.AuthorizationResponse; +import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation; +import org.keycloak.representations.idm.authorization.PermissionRequest; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.GroupBuilder; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.RolesBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class GroupPathWithoutGroupClaimPolicyTest extends GroupPathPolicyTest { + + @Override + public void addTestRealms(List testRealms) { + ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation(); + + groupProtocolMapper.setName("groups"); + groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID); + groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + groupProtocolMapper.setConsentRequired(false); + Map config = new HashMap<>(); + config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups"); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + groupProtocolMapper.setConfig(config); + + testRealms.add(RealmBuilder.create().name("authz-test") + .roles(RolesBuilder.create() + .realmRole(RoleBuilder.create().name("uma_authorization").build()) + ) + .group(GroupBuilder.create().name("Group A") + .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> { + if ("Group B".equals(name)) { + return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function() { + @Override + public GroupRepresentation apply(String name) { + return GroupBuilder.create().name(name).build(); + } + }).collect(Collectors.toList())).build(); + } + return GroupBuilder.create().name(name).build(); + }).collect(Collectors.toList())).build()) + .group(GroupBuilder.create().name("Group E").build()) + .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A")) + .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization")) + .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization")) + .client(ClientBuilder.create().clientId("resource-server-test") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-test") + .defaultRoles("uma_protection") + .directAccessGrants()) + .build()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java index 9d47f8d931..aa9ce50db7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java @@ -101,9 +101,20 @@ public class PermissionManagementTest extends AbstractResourceServerTest { @Test public void testDeleteResourceAndPermissionTicket() throws Exception { - ResourceRepresentation resource = addResource("Resource A", true); - PermissionResponse response = getAuthzClient().protection().permission().create(new PermissionRequest(resource.getName())); - assertNotNull(response.getTicket()); + ResourceRepresentation resource = addResource("Resource A", "kolo", true, "ScopeA", "ScopeB", "ScopeC"); + AuthzClient authzClient = getAuthzClient(); + PermissionResponse response = authzClient.protection("marta", "password").permission().create(new PermissionRequest(resource.getId(), "ScopeA", "ScopeB", "ScopeC")); + AuthorizationRequest request = new AuthorizationRequest(); + request.setTicket(response.getTicket()); + request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken()); + + try { + authzClient.authorization().authorize(request); + } catch (Exception e) { + + } + + assertPersistence(response, resource, "ScopeA", "ScopeB", "ScopeC"); getAuthzClient().protection().resource().delete(resource.getId()); assertTrue(getAuthzClient().protection().permission().findByResource(resource.getId()).isEmpty()); 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 bedd4a70d5..1e5ec8f428 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 @@ -665,6 +665,6 @@ public class PolicyEvaluationTest extends AbstractAuthzTest { } return baseAttributes; } - }, policy, policy, evaluation -> {}, authorization); + }, policy, evaluation -> {}, authorization); } } 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 5ef774efe8..7274ca52f5 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 @@ -56,7 +56,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { JSPolicyRepresentation policy = new JSPolicyRepresentation(); policy.setName("Only Owner Policy"); - policy.setCode("print($evaluation.getPermission().getResource().getOwner());print($evaluation.getContext().getIdentity().getId());if ($evaluation.getContext().getIdentity().getId() == $evaluation.getPermission().getResource().getOwner()) {$evaluation.grant();}"); + policy.setCode("if ($evaluation.getContext().getIdentity().getId() == $evaluation.getPermission().getResource().getOwner()) {$evaluation.grant();}"); Response response = authorization.policies().js().create(policy); response.close(); @@ -98,6 +98,90 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { } } + /** + * Makes sure permissions granted to a typed resource instance does not grant access to resource instances with the same type. + * + * @throws Exception + */ + @Test + public void testOnlyOwnerCanAccessResourceWithType() throws Exception { + ResourceRepresentation typedResource = addResource("Typed Resource", getClient(getRealm()).toRepresentation().getId(), false, "ScopeA", "ScopeB"); + + typedResource.setType("my:resource"); + + getClient(getRealm()).authorization().resources().resource(typedResource.getId()).update(typedResource); + + resource = addResource("Resource A", "marta", true, "ScopeA", "ScopeB"); + + resource.setType(typedResource.getType()); + + getClient(getRealm()).authorization().resources().resource(resource.getId()).update(resource); + + ResourceRepresentation resourceB = addResource("Resource B", "marta", true, "ScopeA", "ScopeB"); + + resourceB.setType(typedResource.getType()); + + getClient(getRealm()).authorization().resources().resource(resourceB.getId()).update(resourceB); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(resource.getType() + " Permission"); + permission.setResourceType(resource.getType()); + permission.addPolicy("Only Owner Policy"); + + getClient(getRealm()).authorization().permissions().resource().create(permission).close(); + + AuthorizationResponse response = authorize("marta", "password", resource.getName(), new String[] {"ScopeA", "ScopeB"}); + String rpt = response.getToken(); + + assertNotNull(rpt); + assertFalse(response.isUpgraded()); + + AccessToken accessToken = toAccessToken(rpt); + AccessToken.Authorization authorization = accessToken.getAuthorization(); + + assertNotNull(authorization); + + List permissions = authorization.getPermissions(); + + assertNotNull(permissions); + assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB"); + assertTrue(permissions.isEmpty()); + + try { + response = authorize("kolo", "password", resource.getId(), new String[] {"ScopeA", "ScopeB"}); + fail("User should not have access to resource from another user"); + } catch (AuthorizationDeniedException ade) { + + } + + List tickets = getAuthzClient().protection().permission().find(resource.getId(), null, null, null, null, null, null, null); + + for (PermissionTicketRepresentation ticket : tickets) { + ticket.setGranted(true); + getAuthzClient().protection().permission().update(ticket); + } + + try { + response = authorize("kolo", "password", resource.getId(), new String[] {"ScopeA", "ScopeB"}); + } catch (AuthorizationDeniedException ade) { + fail("User should have access to resource from another user"); + } + + permissions = authorization.getPermissions(); + + assertNotNull(permissions); + assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB"); + assertTrue(permissions.isEmpty()); + + try { + response = authorize("kolo", "password", resourceB.getId(), new String[] {"ScopeA", "ScopeB"}); + fail("User should not have access to resource from another user"); + } catch (AuthorizationDeniedException ade) { + + } + } + @Test public void testUserGrantsAccessToResource() throws Exception { ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); @@ -306,7 +390,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest { try { response = authorize("kolo", "password", resource.getId(), new String[] {"ScopeA"}); - fail("User should have access to resource from another user"); + fail("User should not have access to resource from another user"); } catch (AuthorizationDeniedException ade) { }