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 da18f66e3a..fc5981d1cb 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 @@ -87,6 +87,9 @@ public class StoreFactoryCacheManager extends CacheManager { if (type != null) { invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, serverId)); + invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, owner, serverId)); + invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, null, serverId)); + invalidations.add(StoreFactoryCacheSession.getResourceByTypeInstanceCacheKey(type, serverId)); addInvalidations(InResourcePredicate.create().resource(type), 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 2990881f55..1f89ed67c4 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 @@ -360,6 +360,14 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { return "resource.type." + type + "." + serverId; } + public static String getResourceByTypeCacheKey(String type, String owner, String serverId) { + return "resource.type." + type + ".owner." + owner + "." + serverId; + } + + public static String getResourceByTypeInstanceCacheKey(String type, String serverId) { + return "resource.type.instance." + type + "." + serverId; + } + public static String getResourceByUriCacheKey(String uri, String serverId) { return "resource.uri." + uri + "." + serverId; } @@ -725,6 +733,42 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer); } + @Override + public List findByType(String type, String owner, String resourceServerId) { + if (resourceServerId.equals(owner)) { + return findByType(type, resourceServerId); + } else { + if (type == null) return Collections.emptyList(); + String cacheKey = getResourceByTypeCacheKey(type, owner, resourceServerId); + return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByType(type, owner, resourceServerId), + (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId); + } + } + + @Override + public void findByType(String type, String owner, String resourceServerId, Consumer consumer) { + if (type == null) return; + String cacheKey = getResourceByTypeCacheKey(type, owner, resourceServerId); + cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByType(type, owner, resourceServerId), + (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer); + } + + @Override + public List findByTypeInstance(String type, String resourceServerId) { + if (type == null) return Collections.emptyList(); + String cacheKey = getResourceByTypeInstanceCacheKey(type, resourceServerId); + return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByTypeInstance(type, resourceServerId), + (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId); + } + + @Override + public void findByTypeInstance(String type, String resourceServerId, Consumer consumer) { + if (type == null) return; + String cacheKey = getResourceByTypeInstanceCacheKey(type, resourceServerId); + cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByTypeInstance(type, resourceServerId), + (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer); + } + private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId) { return cacheQuery(cacheKey, queryType, resultSupplier, querySupplier, resourceServerId, null); } 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 4f921548e4..33640771f2 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 @@ -62,6 +62,8 @@ import org.hibernate.annotations.FetchMode; @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and :uri in elements(r.uris)"), @NamedQuery(name="findResourceIdByName", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.name = :name"), @NamedQuery(name="findResourceIdByType", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.type = :type"), + @NamedQuery(name="findResourceIdByTypeNoOwner", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.type = :type"), + @NamedQuery(name="findResourceIdByTypeInstance", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.type = :type and r.owner <> :serverId"), @NamedQuery(name="findResourceIdByServerId", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId "), @NamedQuery(name="findResourceIdByScope", query="select r 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/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java index 7a95f7dd97..2793b2b239 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 @@ -39,7 +39,6 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Consumer; /** @@ -289,13 +288,61 @@ public class JPAResourceStore implements ResourceStore { return list; } + @Override + public List findByType(String type, String owner, String resourceServerId) { + List list = new LinkedList<>(); + + findByType(type, owner, resourceServerId, list::add); + + return list; + } + @Override public void findByType(String type, String resourceServerId, Consumer consumer) { - TypedQuery query = entityManager.createNamedQuery("findResourceIdByType", ResourceEntity.class); + findByType(type, resourceServerId, resourceServerId, consumer); + } + + @Override + public void findByType(String type, String owner, String resourceServerId, Consumer consumer) { + TypedQuery query; + + if (owner != null) { + query = entityManager.createNamedQuery("findResourceIdByType", ResourceEntity.class); + } else { + query = entityManager.createNamedQuery("findResourceIdByTypeNoOwner", ResourceEntity.class); + } + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("type", type); + + if (owner != null) { + query.setParameter("ownerId", owner); + } + + query.setParameter("serverId", resourceServerId); + + StoreFactory storeFactory = provider.getStoreFactory(); + + query.getResultList().stream() + .map(entity -> new ResourceAdapter(entity, entityManager, storeFactory)) + .forEach(consumer); + } + + @Override + public List findByTypeInstance(String type, String resourceServerId) { + List list = new LinkedList<>(); + + findByTypeInstance(type, resourceServerId, list::add); + + return list; + } + + @Override + public void findByTypeInstance(String type, String resourceServerId, Consumer consumer) { + TypedQuery query = entityManager.createNamedQuery("findResourceIdByTypeInstance", ResourceEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", type); - query.setParameter("ownerId", resourceServerId); query.setParameter("serverId", resourceServerId); StoreFactory storeFactory = provider.getStoreFactory(); 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 bd1d88d088..182c34b9d7 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 @@ -535,6 +535,26 @@ public final class AuthorizationProvider implements Provider { public void findByType(String type, String resourceServerId, Consumer consumer) { delegate.findByType(type, resourceServerId, consumer); } + + @Override + public void findByType(String type, String owner, String resourceServerId, Consumer consumer) { + delegate.findByType(type, owner, resourceServerId, consumer); + } + + @Override + public List findByType(String type, String owner, String resourceServerId) { + return delegate.findByType(type, resourceServerId); + } + + @Override + public List findByTypeInstance(String type, String resourceServerId) { + return delegate.findByTypeInstance(type, resourceServerId); + } + + @Override + public void findByTypeInstance(String type, String resourceServerId, Consumer consumer) { + delegate.findByTypeInstance(type, resourceServerId, consumer); + } }; } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java index d9e652b7f2..b4b7c58cae 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java @@ -139,6 +139,15 @@ public interface ResourceStore { */ List findByType(String type, String resourceServerId); + /** + * Finds all {@link Resource} with the given type. + * + * @param type the type of the resource + * @param owner the resource owner or null for any resource with a given type + * @return a list of resources with the given type + */ + List findByType(String type, String owner, String resourceServerId); + /** * Finds all {@link Resource} with the given type. * @@ -148,4 +157,19 @@ public interface ResourceStore { * @return a list of resources with the given type */ void findByType(String type, String resourceServerId, Consumer consumer); + + /** + * Finds all {@link Resource} with the given type. + * + * @param type the type of the resource + * @param owner the resource owner or null for any resource with a given type + * @param resourceServerId the resource server id + * @param consumer the result consumer + * @return a list of resources with the given type + */ + void findByType(String type, String owner, String resourceServerId, Consumer consumer); + + List findByTypeInstance(String type, String resourceServerId); + + void findByTypeInstance(String type, String resourceServerId, Consumer consumer); } diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index 5d6e65c8b4..7ea56b2b8b 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -410,6 +410,22 @@ public class AuthorizationTokenService { if (resource != null) { requestedResources.add(resource); + } else if (resourceId.startsWith("resource-type:")) { + // only resource types, no resource instances. resource types are owned by the resource server + String resourceType = resourceId.substring("resource-type:".length()); + resourceStore.findByType(resourceType, resourceServer.getId(), resourceServer.getId(), requestedResources::add); + } else if (resourceId.startsWith("resource-type-any:")) { + // any resource with a given type + String resourceType = resourceId.substring("resource-type-any:".length()); + resourceStore.findByType(resourceType, null, resourceServer.getId(), requestedResources::add); + } else if (resourceId.startsWith("resource-type-instance:")) { + // only resource instances with a given type + String resourceType = resourceId.substring("resource-type-instance:".length()); + resourceStore.findByTypeInstance(resourceType, resourceServer.getId(), requestedResources::add); + } else if (resourceId.startsWith("resource-type-owner:")) { + // only resources where the current identity is the owner + String resourceType = resourceId.substring("resource-type-owner:".length()); + resourceStore.findByType(resourceType, identity.getId(), resourceServer.getId(), requestedResources::add); } else { String resourceName = resourceId; Resource ownerResource = resourceStore.findByName(resourceName, identity.getId(), resourceServer.getId()); 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 bfc409ec84..fd9a440b3d 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -155,11 +155,9 @@ public final class Permissions { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); resourceStore.findByType(type, resourceServer.getId(), resource1 -> { - if (resource1.getOwner().equals(resourceServer.getId())) { - for (Scope typeScope : resource1.getScopes()) { - if (!scopes.contains(typeScope)) { - scopes.add(typeScope); - } + for (Scope typeScope : resource1.getScopes()) { + if (!scopes.contains(typeScope)) { + scopes.add(typeScope); } } }); 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 27b3d41344..21e6a4e6bd 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 @@ -51,6 +51,7 @@ 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.admin.client.resource.ResourceResource; import org.keycloak.authorization.client.AuthorizationDeniedException; import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.Configuration; @@ -841,6 +842,189 @@ public class EntitlementAPITest extends AbstractAuthzTest { } } + @Test + public void testObtainAllEntitlementsForResourceType() throws Exception { + ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST); + AuthorizationResource authorization = client.authorization(); + + JSPolicyRepresentation policy = new JSPolicyRepresentation(); + + policy.setName(KeycloakModelUtils.generateId()); + policy.setCode("$evaluation.grant();"); + + authorization.policies().js().create(policy).close(); + + for (int i = 0; i < 10; i++) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setType("type-one"); + resource.setName(KeycloakModelUtils.generateId()); + + authorization.resources().create(resource).close(); + } + + for (int i = 0; i < 10; i++) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setType("type-two"); + resource.setName(KeycloakModelUtils.generateId()); + + authorization.resources().create(resource).close(); + } + + for (int i = 0; i < 10; i++) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setType("type-three"); + resource.setName(KeycloakModelUtils.generateId()); + + authorization.resources().create(resource).close(); + } + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(KeycloakModelUtils.generateId()); + permission.setResourceType("type-one"); + permission.addPolicy(policy.getName()); + + authorization.permissions().resource().create(permission).close(); + + permission = new ResourcePermissionRepresentation(); + + permission.setName(KeycloakModelUtils.generateId()); + permission.setResourceType("type-two"); + permission.addPolicy(policy.getName()); + + authorization.permissions().resource().create(permission).close(); + + permission = new ResourcePermissionRepresentation(); + + permission.setName(KeycloakModelUtils.generateId()); + permission.setResourceType("type-three"); + permission.addPolicy(policy.getName()); + + authorization.permissions().resource().create(permission).close(); + + String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken(); + AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG); + + AuthorizationRequest request = new AuthorizationRequest(); + request.addPermission("resource-type:type-one"); + AuthorizationResponse response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + Collection permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(10, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type:type-three"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(10, permissions.size()); + + + for (int i = 0; i < 5; i++) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setOwner("kolo"); + resource.setType("type-two"); + resource.setName(KeycloakModelUtils.generateId()); + + authorization.resources().create(resource).close(); + } + + request = new AuthorizationRequest(); + request.addPermission("resource-type-any:type-two"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(15, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-owner:type-two"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(5, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-instance:type-two"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(5, permissions.size()); + + Permission next = permissions.iterator().next(); + + ResourceResource resourceMgmt = client.authorization().resources().resource(next.getResourceId()); + ResourceRepresentation representation = resourceMgmt.toRepresentation(); + + representation.setType("type-three"); + + resourceMgmt.update(representation); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-instance:type-two"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(4, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-instance:type-three"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(1, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-any:type-three"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(11, permissions.size()); + + for (int i = 0; i < 2; i++) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setOwner("marta"); + resource.setType("type-one"); + resource.setName(KeycloakModelUtils.generateId()); + + authorization.resources().create(resource).close(); + } + + request = new AuthorizationRequest(); + request.addPermission("resource-type:type-one"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(10, permissions.size()); + + accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken(); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-owner:type-one"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(2, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-instance:type-one"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(2, permissions.size()); + + request = new AuthorizationRequest(); + request.addPermission("resource-type-any:type-one"); + response = authzClient.authorization(accessToken).authorize(request); + assertNotNull(response.getToken()); + permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions(); + assertEquals(12, permissions.size()); + } + @Test public void testOverridePermission() throws Exception { ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);