From 1e2db74d866bda50f3d78d42892f16e2d7efe4e7 Mon Sep 17 00:00:00 2001 From: Michal Hajas Date: Mon, 22 Mar 2021 15:49:23 +0100 Subject: [PATCH] KEYCLOAK-16932 Authorization map storage --- .github/workflows/ci.yml | 2 +- .../authorization/PolicyAdapter.java | 5 - .../authorization/ResourceAdapter.java | 5 - .../StoreFactoryCacheSession.java | 14 +- .../jpa/entities/PermissionTicketEntity.java | 2 +- .../jpa/store/JPAPermissionTicketStore.java | 93 ++--- .../jpa/store/JPAPolicyStore.java | 87 ++--- .../jpa/store/JPAResourceStore.java | 105 ++---- .../jpa/store/JPAScopeStore.java | 17 +- .../jpa/store/PolicyAdapter.java | 5 - .../jpa/store/ResourceAdapter.java | 6 - .../authorization/MapAuthorizationStore.java | 101 ++++++ .../MapAuthorizationStoreFactory.java | 92 +++++ .../MapPermissionTicketStore.java | 320 ++++++++++++++++++ .../map/authorization/MapPolicyStore.java | 264 +++++++++++++++ .../authorization/MapResourceServerStore.java | 131 +++++++ .../map/authorization/MapResourceStore.java | 270 +++++++++++++++ .../map/authorization/MapScopeStore.java | 163 +++++++++ .../AbstractPermissionTicketModel.java | 52 +++ .../adapter/AbstractPolicyModel.java | 51 +++ .../adapter/AbstractResourceModel.java | 51 +++ .../adapter/AbstractResourceServerModel.java | 51 +++ .../adapter/AbstractScopeModel.java | 51 +++ .../adapter/MapPermissionTicketAdapter.java | 107 ++++++ .../adapter/MapPolicyAdapter.java | 197 +++++++++++ .../adapter/MapResourceAdapter.java | 164 +++++++++ .../adapter/MapResourceServerAdapter.java | 74 ++++ .../adapter/MapScopeAdapter.java | 78 +++++ .../AbstractPermissionTicketEntity.java | 127 +++++++ .../entity/AbstractPolicyEntity.java | 191 +++++++++++ .../entity/AbstractResourceEntity.java | 183 ++++++++++ .../entity/AbstractResourceServerEntity.java | 79 +++++ .../entity/AbstractScopeEntity.java | 86 +++++ .../entity/MapPermissionTicketEntity.java | 41 +++ .../authorization/entity/MapPolicyEntity.java | 35 ++ .../entity/MapResourceEntity.java | 38 +++ .../entity/MapResourceServerEntity.java | 33 ++ .../authorization/entity/MapScopeEntity.java | 35 ++ .../map/storage/MapFieldPredicates.java | 167 ++++++++- .../map/storage/MapModelCriteriaBuilder.java | 5 +- .../models/map/user/MapUserProvider.java | 6 +- ...horization.store.AuthorizationStoreFactory | 19 ++ .../authorization/AuthorizationProvider.java | 6 +- .../UserManagedPermissionUtil.java | 23 +- .../model/AbstractAuthorizationModel.java | 5 +- .../authorization/model/PermissionTicket.java | 55 ++- .../keycloak/authorization/model/Policy.java | 46 ++- .../authorization/model/Resource.java | 46 ++- .../authorization/model/ResourceServer.java | 5 + .../keycloak/authorization/model/Scope.java | 30 ++ ...ionTicketAwareDecisionResultCollector.java | 18 +- .../store/PermissionTicketStore.java | 21 +- .../authorization/store/PolicyStore.java | 40 ++- .../authorization/store/ResourceStore.java | 54 ++- .../authorization/store/ScopeStore.java | 16 +- .../ClientApplicationSynchronizer.java | 8 +- .../syncronization/GroupSynchronizer.java | 8 +- .../syncronization/UserSynchronizer.java | 16 +- .../java/org/keycloak/utils/StreamsUtil.java | 15 + .../admin/PermissionService.java | 10 +- .../authorization/admin/PolicyService.java | 34 +- .../admin/PolicyTypeService.java | 4 +- .../admin/ResourceSetService.java | 35 +- .../authorization/admin/ScopeService.java | 8 +- .../PolicyEvaluationResponseBuilder.java | 5 +- .../permission/PermissionTicketService.java | 28 +- .../freemarker/model/AuthorizationBean.java | 41 +-- .../resources/account/AccountFormService.java | 30 +- .../account/resources/ResourceService.java | 23 +- .../account/resources/ResourcesService.java | 23 +- .../keycloak/storage/UserStorageManager.java | 15 +- .../resources/META-INF/keycloak-server.json | 2 +- 72 files changed, 3840 insertions(+), 433 deletions(-) create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPermissionTicketModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPolicyModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceServerModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractScopeModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPermissionTicketAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPolicyAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceServerAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapScopeAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPermissionTicketEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPolicyEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceServerEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractScopeEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37807692c5f..fd9336446c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: run: | declare -A PARAMS TESTGROUP PARAMS["quarkus"]="-Pauth-server-quarkus" - PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map" + PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map -Dkeycloak.authorization.provider=map" PARAMS["wildfly"]="-Pauth-server-wildfly" TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r" TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b" diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java index d54665399b0..311f74f2ece 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java @@ -280,11 +280,6 @@ public class PolicyAdapter implements Policy, CachedModel { } - @Override - public boolean isFetched(String association) { - return modelSupplier.get().isFetched(association); - } - protected Set scopes; @Override 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 a288213f50e..4f4a302077c 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 @@ -268,11 +268,6 @@ public class ResourceAdapter implements Resource, CachedModel { updated.removeAttribute(name); } - @Override - public boolean isFetched(String association) { - return modelSupplier.get().isFetched(association); - } - @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/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index a480a8af866..723e2e4a06d 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 @@ -572,16 +572,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return getScopeStoreDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } } protected class ResourceCache implements ResourceStore { - @Override - public Resource create(String name, ResourceServer resourceServer, String owner) { - return create(null, name, resourceServer, owner); - } @Override public Resource create(String id, String name, ResourceServer resourceServer, String owner) { @@ -706,7 +702,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return getResourceStoreDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } @@ -962,7 +958,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return getPolicyStoreDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } @@ -1115,7 +1111,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { protected class PermissionTicketCache implements PermissionTicketStore { @Override - public long count(Map attributes, String resourceServerId) { + public long count(Map attributes, String resourceServerId) { return getPermissionTicketStoreDelegate().count(attributes, resourceServerId); } @@ -1193,7 +1189,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } @Override - public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { return getPermissionTicketStoreDelegate().find(attributes, resourceServerId, firstResult, maxResult); } 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 de3f59c636d..cd23477d2af 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 @@ -35,7 +35,7 @@ import javax.persistence.UniqueConstraint; */ @Entity @Table(name = "RESOURCE_SERVER_PERM_TICKET", uniqueConstraints = { - @UniqueConstraint(columnNames = {"OWNER", "RESOURCE_SERVER_ID", "RESOURCE_ID", "SCOPE_ID"}) + @UniqueConstraint(columnNames = {"OWNER", "REQUESTER", "RESOURCE_SERVER_ID", "RESOURCE_ID", "SCOPE_ID"}) }) @NamedQueries( { 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 345042bdf24..3aaa1a387a7 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,7 +18,7 @@ package org.keycloak.authorization.jpa.store; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.EnumMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -58,7 +58,7 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { } @Override - public long count(Map attributes, String resourceServerId) { + public long count(Map attributes, String resourceServerId) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery querybuilder = builder.createQuery(Long.class); Root root = querybuilder.from(PermissionTicketEntity.class); @@ -77,46 +77,49 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { private List getPredicates(CriteriaBuilder builder, Root root, String resourceServerId, - Map attributes) { - List predicates = new ArrayList(); + Map attributes) { + List predicates = new ArrayList<>(); if (resourceServerId != null) { predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId)); } - attributes.forEach((name, value) -> { - if (PermissionTicket.ID.equals(name)) { - predicates.add(root.get(name).in(value)); - } else if (PermissionTicket.SCOPE.equals(name)) { - predicates.add(root.join("scope").get("id").in(value)); - } else if (PermissionTicket.SCOPE_IS_NULL.equals(name)) { - if (Boolean.valueOf(value)) { - predicates.add(builder.isNull(root.get("scope"))); - } else { - predicates.add(builder.isNotNull(root.get("scope"))); - } - } else if (PermissionTicket.RESOURCE.equals(name)) { - predicates.add(root.join("resource").get("id").in(value)); - } else if (PermissionTicket.RESOURCE_NAME.equals(name)) { - predicates.add(root.join("resource").get("name").in(value)); - } else if (PermissionTicket.OWNER.equals(name)) { - predicates.add(builder.equal(root.get("owner"), value)); - } else if (PermissionTicket.REQUESTER.equals(name)) { - predicates.add(builder.equal(root.get("requester"), value)); - } else if (PermissionTicket.GRANTED.equals(name)) { - if (Boolean.valueOf(value)) { - predicates.add(builder.isNotNull(root.get("grantedTimestamp"))); - } else { - predicates.add(builder.isNull(root.get("grantedTimestamp"))); - } - } 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 if (PermissionTicket.POLICY.equals(name)) { - predicates.add(root.join("policy").get("id").in(value)); - } else { - throw new RuntimeException("Unsupported filter [" + name + "]"); + attributes.forEach((filterOption, value) -> { + switch (filterOption) { + case ID: + case OWNER: + case REQUESTER: + predicates.add(builder.equal(root.get(filterOption.getName()), value)); + break; + case SCOPE_ID: + case RESOURCE_ID: + case RESOURCE_NAME: + case POLICY_ID: + String[] predicateValues = filterOption.getName().split("\\."); + predicates.add(root.join(predicateValues[0]).get(predicateValues[1]).in(value)); + break; + case SCOPE_IS_NULL: + if (Boolean.parseBoolean(value)) { + predicates.add(builder.isNull(root.get("scope"))); + } else { + predicates.add(builder.isNotNull(root.get("scope"))); + } + break; + case GRANTED: + if (Boolean.parseBoolean(value)) { + predicates.add(builder.isNotNull(root.get("grantedTimestamp"))); + } else { + predicates.add(builder.isNull(root.get("grantedTimestamp"))); + } + break; + case REQUESTER_IS_NULL: + predicates.add(builder.isNull(root.get("requester"))); + break; + case POLICY_IS_NOT_NULL: + predicates.add(builder.isNotNull(root.get("policy"))); + break; + default: + throw new IllegalArgumentException("Unsupported filter [" + filterOption + "]"); } }); return predicates; @@ -235,7 +238,7 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { } @Override - public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery querybuilder = builder.createQuery(PermissionTicketEntity.class); Root root = querybuilder.from(PermissionTicketEntity.class); @@ -264,21 +267,21 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { @Override public List findGranted(String userId, String resourceServerId) { - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - filters.put(PermissionTicket.REQUESTER, userId); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, userId); return find(filters, resourceServerId, -1, -1); } @Override public List findGranted(String resourceName, String userId, String resourceServerId) { - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE_NAME, resourceName); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - filters.put(PermissionTicket.REQUESTER, userId); + filters.put(PermissionTicket.FilterOption.RESOURCE_NAME, resourceName); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, userId); return find(filters, resourceServerId, -1, -1); } 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 6f8364161c2..dcba8cbffd2 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 @@ -136,7 +136,7 @@ public class JPAPolicyStore implements PolicyStore { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery querybuilder = builder.createQuery(PolicyEntity.class); Root root = querybuilder.from(PolicyEntity.class); @@ -147,32 +147,46 @@ public class JPAPolicyStore implements PolicyStore { predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId)); } - attributes.forEach((name, value) -> { - if ("permission".equals(name)) { - if (Boolean.valueOf(value[0])) { - predicates.add(root.get("type").in("resource", "scope", "uma")); - } else { - predicates.add(builder.not(root.get("type").in("resource", "scope", "uma"))); + attributes.forEach((filterOption, value) -> { + switch (filterOption) { + case ID: + case OWNER: + predicates.add(root.get(filterOption.getName()).in(value)); + break; + case SCOPE_ID: + case RESOURCE_ID: + String[] predicateValues = filterOption.getName().split("\\."); + predicates.add(root.join(predicateValues[0]).get(predicateValues[1]).in(value)); + break; + case PERMISSION: { + if (Boolean.parseBoolean(value[0])) { + predicates.add(root.get("type").in("resource", "scope", "uma")); + } else { + predicates.add(builder.not(root.get("type").in("resource", "scope", "uma"))); + } } - } 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 if ("scope".equals(name)) { - predicates.add(root.join("scopes").get("id").in(value)); - } else if (name.startsWith("config:")) { - predicates.add(root.joinMap("config").key().in(name.substring("config:".length()))); - predicates.add(builder.like(root.joinMap("config").value().as(String.class), "%" + value[0] + "%")); - } else { - predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); + break; + case OWNER_IS_NOT_NULL: + predicates.add(builder.isNotNull(root.get("owner"))); + break; + case CONFIG: + if (value.length != 2) { + throw new IllegalArgumentException("Config filter option requires value with two items: [config_name, expected_config_value]"); + } + + predicates.add(root.joinMap("config").key().in(value[0])); + predicates.add(builder.like(root.joinMap("config").value().as(String.class), "%" + value[1] + "%")); + break; + case TYPE: + case NAME: + predicates.add(builder.like(builder.lower(root.get(filterOption.getName())), "%" + value[0].toLowerCase() + "%")); + break; + default: + throw new IllegalArgumentException("Unsupported filter [" + filterOption + "]"); } }); - if (!attributes.containsKey("owner") && !attributes.containsKey("owner_is_not_null")) { + if (!attributes.containsKey(Policy.FilterOption.OWNER) && !attributes.containsKey(Policy.FilterOption.OWNER_IS_NOT_NULL)) { predicates.add(builder.isNull(root.get("owner"))); } @@ -191,15 +205,6 @@ public class JPAPolicyStore implements PolicyStore { return list; } - @Override - public List findByResource(final String resourceId, String resourceServerId) { - List result = new LinkedList<>(); - - findByResource(resourceId, resourceServerId, result::add); - - return result; - } - @Override public void findByResource(String resourceId, String resourceServerId, Consumer consumer) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResource", PolicyEntity.class); @@ -216,15 +221,6 @@ public class JPAPolicyStore implements PolicyStore { .forEach(consumer::accept); } - @Override - public List findByResourceType(final String resourceType, String resourceServerId) { - List result = new LinkedList<>(); - - findByResourceType(resourceType, resourceServerId, result::add); - - return result; - } - @Override public void findByResourceType(String resourceType, String resourceServerId, Consumer consumer) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResourceType", PolicyEntity.class); @@ -262,15 +258,6 @@ public class JPAPolicyStore implements PolicyStore { return list; } - @Override - public List findByScopeIds(List scopeIds, String resourceId, String resourceServerId) { - List result = new LinkedList<>(); - - findByScopeIds(scopeIds, resourceId, resourceServerId, result::add); - - return result; - } - @Override public void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer) { // Use separate subquery to handle DB2 and MSSSQL 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 898e4ca6ff5..2d760d32a9a 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 @@ -56,11 +56,6 @@ public class JPAResourceStore implements ResourceStore { this.provider = provider; } - @Override - public Resource create(String name, ResourceServer resourceServer, String owner) { - return create(null, name, resourceServer, owner); - } - @Override public Resource create(String id, String name, ResourceServer resourceServer, String owner) { ResourceEntity entity = new ResourceEntity(); @@ -101,15 +96,6 @@ public class JPAResourceStore implements ResourceStore { return new ResourceAdapter(entity, entityManager, provider.getStoreFactory()); } - @Override - public List findByOwner(String ownerId, String resourceServerId) { - List list = new LinkedList<>(); - - findByOwner(ownerId, resourceServerId, list::add); - - return list; - } - @Override public void findByOwner(String ownerId, String resourceServerId, Consumer consumer) { findByOwnerFilter(ownerId, resourceServerId, consumer, -1, -1); @@ -195,7 +181,7 @@ public class JPAResourceStore implements ResourceStore { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery querybuilder = builder.createQuery(ResourceEntity.class); Root root = querybuilder.from(ResourceEntity.class); @@ -206,29 +192,36 @@ public class JPAResourceStore implements ResourceStore { predicates.add(builder.equal(root.get("resourceServer"), resourceServerId)); } - attributes.forEach((name, value) -> { - if ("id".equals(name)) { - predicates.add(root.get(name).in(value)); - } else if ("scope".equals(name)) { - predicates.add(root.join("scopes").get("id").in(value)); - } else if ("ownerManagedAccess".equals(name) && value.length > 0) { - predicates.add(builder.equal(root.get(name), Boolean.valueOf(value[0]))); - } else if ("uri".equals(name) && value.length > 0 && value[0] != null) { - predicates.add(builder.lower(root.join("uris")).in(value[0].toLowerCase())); - } else if ("uri_not_null".equals(name)) { - // predicates.add(builder.isNotEmpty(root.get("uris"))); looks like there is a bug in hibernate and this line doesn't work: https://hibernate.atlassian.net/browse/HHH-6686 - // Workaround - Expression urisSize = builder.size(root.get("uris")); - predicates.add(builder.notEqual(urisSize, 0)); - } else if ("owner".equals(name)) { - predicates.add(root.get(name).in(value)); - } else if (!Resource.EXACT_NAME.equals(name)) { - if ("name".equals(name) && attributes.containsKey(Resource.EXACT_NAME) && Boolean.valueOf(attributes.get(Resource.EXACT_NAME)[0]) - && value.length > 0 && value[0] != null) { - predicates.add(builder.equal(builder.lower(root.get(name)), value[0].toLowerCase())); - } else if (value.length > 0 && value[0] != null) { - predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); - } + attributes.forEach((filterOption, value) -> { + switch (filterOption) { + case ID: + case OWNER: + predicates.add(root.get(filterOption.getName()).in(value)); + break; + case SCOPE_ID: + predicates.add(root.join("scopes").get("id").in(value)); + break; + case OWNER_MANAGED_ACCESS: + predicates.add(builder.equal(root.get(filterOption.getName()), Boolean.valueOf(value[0]))); + break; + case URI: + predicates.add(builder.lower(root.join("uris")).in(value[0].toLowerCase())); + break; + case URI_NOT_NULL: + // predicates.add(builder.isNotEmpty(root.get("uris"))); looks like there is a bug in hibernate and this line doesn't work: https://hibernate.atlassian.net/browse/HHH-6686 + // Workaround + Expression urisSize = builder.size(root.get("uris")); + predicates.add(builder.notEqual(urisSize, 0)); + break; + case NAME: + case TYPE: + predicates.add(builder.like(builder.lower(root.get(filterOption.getName())), "%" + value[0].toLowerCase() + "%")); + break; + case EXACT_NAME: + predicates.add(builder.equal(builder.lower(root.get(filterOption.getName())), value[0].toLowerCase())); + break; + default: + throw new IllegalArgumentException("Unsupported filter [" + filterOption + "]"); } }); @@ -251,15 +244,6 @@ public class JPAResourceStore implements ResourceStore { return list; } - @Override - public List findByScope(List scopes, String resourceServerId) { - List result = new ArrayList<>(); - - findByScope(scopes, resourceServerId, result::add); - - return result; - } - @Override public void findByScope(List scopes, String resourceServerId, Consumer consumer) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByScope", ResourceEntity.class); @@ -295,24 +279,6 @@ public class JPAResourceStore implements ResourceStore { } } - @Override - public List findByType(String type, String resourceServerId) { - List list = new LinkedList<>(); - - findByType(type, resourceServerId, list::add); - - 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) { findByType(type, resourceServerId, resourceServerId, consumer); @@ -344,15 +310,6 @@ public class JPAResourceStore implements ResourceStore { .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); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java index 61a566070ae..144347c66b3 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java @@ -131,7 +131,7 @@ public class JPAScopeStore implements ScopeStore { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery querybuilder = builder.createQuery(ScopeEntity.class); Root root = querybuilder.from(ScopeEntity.class); @@ -140,11 +140,16 @@ public class JPAScopeStore implements ScopeStore { predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId)); - attributes.forEach((name, value) -> { - if ("id".equals(name)) { - predicates.add(root.get(name).in(value)); - } else { - predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); + attributes.forEach((filterOption, value) -> { + switch (filterOption) { + case ID: + predicates.add(root.get(filterOption.getName()).in(value)); + break; + case NAME: + predicates.add(builder.like(builder.lower(root.get(filterOption.getName())), "%" + value[0].toLowerCase() + "%")); + break; + default: + throw new IllegalArgumentException("Unsupported filter [" + filterOption + "]"); } }); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PolicyAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PolicyAdapter.java index 4746b20be5b..7df7576b99f 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PolicyAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PolicyAdapter.java @@ -253,9 +253,4 @@ public class PolicyAdapter extends AbstractAuthorizationModel implements Policy, return em.getReference(PolicyEntity.class, policy.getId()); } } - - @Override - public boolean isFetched(String association) { - return em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity, association); - } } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java index 3981755f0f2..d142050902c 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java @@ -241,12 +241,6 @@ public class ResourceAdapter extends AbstractAuthorizationModel implements Resou entity.getAttributes().removeAll(toRemove); } - @Override - public boolean isFetched(String association) { - return em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(this.entity, association); - } - - public static ResourceEntity toEntity(EntityManager em, Resource resource) { if (resource instanceof ResourceAdapter) { return ((ResourceAdapter)resource).getEntity(); diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java new file mode 100644 index 00000000000..833901ad8d7 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; +import org.keycloak.models.map.authorization.entity.MapPolicyEntity; +import org.keycloak.models.map.authorization.entity.MapResourceEntity; +import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; +import org.keycloak.models.map.authorization.entity.MapScopeEntity; +import org.keycloak.models.map.storage.MapStorage; + +import java.util.UUID; + +/** + * @author mhajas + */ +public class MapAuthorizationStore implements StoreFactory { + + private final PolicyStore policyStore; + private final ResourceServerStore resourceServerStore; + private final ResourceStore resourceStore; + private final ScopeStore scopeStore; + private final PermissionTicketStore permissionTicketStore; + private boolean readOnly; + + public MapAuthorizationStore(KeycloakSession session, MapStorage permissionTicketStore, MapStorage policyStore, MapStorage resourceServerStore, MapStorage resourceStore, MapStorage scopeStore, AuthorizationProvider provider) { + this.permissionTicketStore = new MapPermissionTicketStore(session, permissionTicketStore, provider); + this.policyStore = new MapPolicyStore(session, policyStore, provider); + this.resourceServerStore = new MapResourceServerStore(session, resourceServerStore, provider); + this.resourceStore = new MapResourceStore(session, resourceStore, provider); + this.scopeStore = new MapScopeStore(session, scopeStore, provider); + } + + @Override + public ResourceStore getResourceStore() { + return resourceStore; + } + + @Override + public ResourceServerStore getResourceServerStore() { + return resourceServerStore; + } + + @Override + public ScopeStore getScopeStore() { + return scopeStore; + } + + @Override + public PolicyStore getPolicyStore() { + return policyStore; + } + + @Override + public PermissionTicketStore getPermissionTicketStore() { + return permissionTicketStore; + } + + @Override + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + @Override + public void close() { + + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java new file mode 100644 index 00000000000..30fe92d2a32 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.keycloak.Config; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.AuthorizationStoreFactory; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; +import org.keycloak.models.map.authorization.entity.MapPolicyEntity; +import org.keycloak.models.map.authorization.entity.MapResourceEntity; +import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; +import org.keycloak.models.map.authorization.entity.MapScopeEntity; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; + +import java.util.UUID; + +/** + * @author mhajas + */ +public class MapAuthorizationStoreFactory implements AuthorizationStoreFactory { + + private MapStorage permissionTicketStore; + private MapStorage policyStore; + private MapStorage resourceServerStore; + private MapStorage resourceStore; + private MapStorage scopeStore; + + @Override + public StoreFactory create(KeycloakSession session) { + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + return new MapAuthorizationStore(session, + permissionTicketStore, + policyStore, + resourceServerStore, + resourceStore, + scopeStore, + provider + ); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + AuthorizationStoreFactory.super.postInit(factory); + + MapStorageProvider mapStorageProvider = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class); + permissionTicketStore = mapStorageProvider.getStorage("authzPermissionTickets", UUID.class, MapPermissionTicketEntity.class, PermissionTicket.class); + policyStore = mapStorageProvider.getStorage("authzPolicies", UUID.class, MapPolicyEntity.class, Policy.class); + resourceServerStore = mapStorageProvider.getStorage("authzResourceServers", String.class, MapResourceServerEntity.class, ResourceServer.class); + resourceStore = mapStorageProvider.getStorage("authzResources", UUID.class, MapResourceEntity.class, Resource.class); + scopeStore = mapStorageProvider.getStorage("authzScopes", UUID.class, MapScopeEntity.class, Scope.class); + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "map"; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java new file mode 100644 index 00000000000..07c99632c7e --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java @@ -0,0 +1,320 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.PermissionTicket.SearchableFields; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.authorization.adapter.MapPermissionTicketAdapter; +import org.keycloak.models.map.authorization.entity.AbstractPermissionTicketEntity; +import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.utils.StreamsUtil.distinctByKey; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +public class MapPermissionTicketStore implements PermissionTicketStore { + + private static final Logger LOG = Logger.getLogger(MapPermissionTicketStore.class); + private final AuthorizationProvider authorizationProvider; + final MapKeycloakTransaction tx; + private final MapStorage permissionTicketStore; + + public MapPermissionTicketStore(KeycloakSession session, MapStorage permissionTicketStore, AuthorizationProvider provider) { + this.authorizationProvider = provider; + this.permissionTicketStore = permissionTicketStore; + this.tx = permissionTicketStore.createTransaction(session); + session.getTransactionManager().enlist(tx); + } + + private MapPermissionTicketEntity registerEntityForChanges(MapPermissionTicketEntity origEntity) { + final MapPermissionTicketEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.updateIfChanged(origEntity.getId(), res, MapPermissionTicketEntity::isUpdated); + return res; + } + + private PermissionTicket entityToAdapter(MapPermissionTicketEntity origEntity) { + if (origEntity == null) return null; + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return new MapPermissionTicketAdapter(registerEntityForChanges(origEntity), authorizationProvider.getStoreFactory()); + } + + private ModelCriteriaBuilder forResourceServer(String resourceServerId) { + ModelCriteriaBuilder mcb = permissionTicketStore.createCriteriaBuilder(); + + return resourceServerId == null + ? mcb + : mcb.compare(SearchableFields.RESOURCE_SERVER_ID, Operator.EQ, + resourceServerId); + } + + @Override + public long count(Map attributes, String resourceServerId) { + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId).and( + attributes.entrySet().stream() + .map(this::filterEntryToModelCriteriaBuilder) + .toArray(ModelCriteriaBuilder[]::new) + ); + + return tx.getCount(mcb); + } + + @Override + public PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer) { + LOG.tracef("create(%s, %s, %s, %s)%s", resourceId, scopeId, requester, resourceServer, getShortStackTrace()); + + String owner = authorizationProvider.getStoreFactory().getResourceStore().findById(resourceId, resourceServer.getId()).getOwner(); + + // @UniqueConstraint(columnNames = {"OWNER", "REQUESTER", "RESOURCE_SERVER_ID", "RESOURCE_ID", "SCOPE_ID"}) + ModelCriteriaBuilder mcb = forResourceServer(resourceServer.getId()) + .compare(SearchableFields.OWNER, Operator.EQ, owner) + .compare(SearchableFields.RESOURCE_ID, Operator.EQ, resourceId) + .compare(SearchableFields.REQUESTER, Operator.EQ, requester); + + if (scopeId != null) { + mcb = mcb.compare(SearchableFields.SCOPE_ID, Operator.EQ, scopeId); + } + + if (tx.getCount(mcb) > 0) { + throw new ModelDuplicateException("Permission ticket for resource server: '" + resourceServer.getId() + + ", Resource: " + resourceId + ", owner: " + owner + ", scopeId: " + scopeId + " already exists."); + } + + MapPermissionTicketEntity entity = new MapPermissionTicketEntity(UUID.randomUUID()); + entity.setResourceId(UUID.fromString(resourceId)); + entity.setRequester(requester); + entity.setCreatedTimestamp(System.currentTimeMillis()); + + if (scopeId != null) { + entity.setScopeId(UUID.fromString(scopeId)); + } + + entity.setOwner(owner); + entity.setResourceServerId(resourceServer.getId()); + + tx.create(entity.getId(), entity); + + return entityToAdapter(entity); + } + + @Override + public void delete(String id) { + LOG.tracef("delete(%s)%s", id, getShortStackTrace()); + tx.delete(UUID.fromString(id)); + } + + @Override + public PermissionTicket findById(String id, String resourceServerId) { + LOG.tracef("findById(%s, %s)%s", id, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(PermissionTicket.SearchableFields.ID, Operator.EQ, id)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public List findByResourceServer(String resourceServerId) { + LOG.tracef("findByResourceServer(%s)%s", resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByOwner(String owner, String resourceServerId) { + LOG.tracef("findByOwner(%s, %s)%s", owner, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.OWNER, Operator.EQ, owner)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByResource(String resourceId, String resourceServerId) { + LOG.tracef("findByResource(%s, %s)%s", resourceId, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.RESOURCE_ID, Operator.EQ, resourceId)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByScope(String scopeId, String resourceServerId) { + LOG.tracef("findByScope(%s, %s)%s", scopeId, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.SCOPE_ID, Operator.EQ, scopeId)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId); + + if (attributes.containsKey(PermissionTicket.FilterOption.RESOURCE_NAME)) { + String expectedResourceName = attributes.remove(PermissionTicket.FilterOption.RESOURCE_NAME); + + Map filterOptionStringMap = new EnumMap<>(Resource.FilterOption.class); + + filterOptionStringMap.put(Resource.FilterOption.EXACT_NAME, new String[]{expectedResourceName}); + + List r = authorizationProvider.getStoreFactory().getResourceStore().findByResourceServer(filterOptionStringMap, resourceServerId, -1, -1); + if (r == null || r.isEmpty()) { + return Collections.emptyList(); + } + mcb = mcb.compare(SearchableFields.RESOURCE_ID, Operator.IN, r.stream().map(Resource::getId).collect(Collectors.toList())); + } + + mcb = mcb.and( + attributes.entrySet().stream() + .map(this::filterEntryToModelCriteriaBuilder) + .toArray(ModelCriteriaBuilder[]::new) + ); + + return paginatedStream(tx.getUpdatedNotRemoved(mcb) + .sorted(MapPermissionTicketEntity.COMPARE_BY_ID), firstResult, maxResult) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + private ModelCriteriaBuilder filterEntryToModelCriteriaBuilder(Map.Entry entry) { + PermissionTicket.FilterOption name = entry.getKey(); + String value = entry.getValue(); + + switch (name) { + case ID: + case SCOPE_ID: + case RESOURCE_ID: + case OWNER: + case REQUESTER: + case POLICY_ID: + return permissionTicketStore.createCriteriaBuilder() + .compare(name.getSearchableModelField(), Operator.EQ, value); + case SCOPE_IS_NULL: + case GRANTED: + case REQUESTER_IS_NULL: { + Operator op = Operator.NOT_EXISTS; + if (Boolean.parseBoolean(value)) { + op = Operator.EXISTS; + } + return permissionTicketStore.createCriteriaBuilder() + .compare(name.getSearchableModelField(), op); + } + case POLICY_IS_NOT_NULL: + return permissionTicketStore.createCriteriaBuilder() + .compare(SearchableFields.REQUESTER, Operator.NOT_EXISTS); + default: + throw new IllegalArgumentException("Unsupported filter [" + name + "]"); + + } + } + + @Override + public List findGranted(String userId, String resourceServerId) { + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); + + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, userId); + + return find(filters, resourceServerId, -1, -1); + } + + @Override + public List findGranted(String resourceName, String userId, String resourceServerId) { + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); + + filters.put(PermissionTicket.FilterOption.RESOURCE_NAME, resourceName); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, userId); + + return find(filters, resourceServerId, -1, -1); + } + + @Override + public List findGrantedResources(String requester, String name, int first, int max) { + ModelCriteriaBuilder mcb = permissionTicketStore.createCriteriaBuilder() + .compare(SearchableFields.REQUESTER, Operator.EQ, requester) + .compare(SearchableFields.GRANTED_TIMESTAMP, Operator.EXISTS); + + Function ticketResourceMapper; + + ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore(); + if (name != null) { + ticketResourceMapper = ticket -> { + Map filterOptionMap = new EnumMap<>(Resource.FilterOption.class); + + filterOptionMap.put(Resource.FilterOption.ID, new String[] {ticket.getResourceId().toString()}); + filterOptionMap.put(Resource.FilterOption.NAME, new String[] {name}); + + List resource = resourceStore.findByResourceServer(filterOptionMap, ticket.getResourceServerId(), -1, 1); + + return resource.isEmpty() ? null : resource.get(0); + }; + } else { + ticketResourceMapper = ticket -> resourceStore + .findById(ticket.getResourceId().toString(), ticket.getResourceServerId()); + } + + return paginatedStream(tx.getUpdatedNotRemoved(mcb) + .filter(distinctByKey(AbstractPermissionTicketEntity::getResourceId)) + .sorted(MapPermissionTicketEntity.COMPARE_BY_RESOURCE_ID) + .map(ticketResourceMapper) + .filter(Objects::nonNull), first, max) + .collect(Collectors.toList()); + } + + @Override + public List findGrantedOwnerResources(String owner, int first, int max) { + ModelCriteriaBuilder mcb = permissionTicketStore.createCriteriaBuilder() + .compare(SearchableFields.OWNER, Operator.EQ, owner); + + return paginatedStream(tx.getUpdatedNotRemoved(mcb) + .filter(distinctByKey(AbstractPermissionTicketEntity::getResourceId)) + .sorted(MapPermissionTicketEntity.COMPARE_BY_RESOURCE_ID), first, max) + .map(ticket -> authorizationProvider.getStoreFactory().getResourceStore() + .findById(ticket.getResourceId().toString(), ticket.getResourceServerId())) + .collect(Collectors.toList()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java new file mode 100644 index 00000000000..b4e56417b2b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java @@ -0,0 +1,264 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Policy.SearchableFields; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.authorization.adapter.MapPolicyAdapter; +import org.keycloak.models.map.authorization.entity.AbstractPolicyEntity; +import org.keycloak.models.map.authorization.entity.MapPolicyEntity; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; +import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +public class MapPolicyStore implements PolicyStore { + + private static final Logger LOG = Logger.getLogger(MapPolicyStore.class); + private final AuthorizationProvider authorizationProvider; + final MapKeycloakTransaction tx; + private final MapStorage policyStore; + + public MapPolicyStore(KeycloakSession session, MapStorage policyStore, AuthorizationProvider provider) { + this.authorizationProvider = provider; + this.policyStore = policyStore; + this.tx = policyStore.createTransaction(session); + session.getTransactionManager().enlist(tx); + } + + private MapPolicyEntity registerEntityForChanges(MapPolicyEntity origEntity) { + final MapPolicyEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.updateIfChanged(origEntity.getId(), res, MapPolicyEntity::isUpdated); + return res; + } + + private Policy entityToAdapter(MapPolicyEntity origEntity) { + if (origEntity == null) return null; + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return new MapPolicyAdapter(registerEntityForChanges(origEntity), authorizationProvider.getStoreFactory()); + } + + private ModelCriteriaBuilder forResourceServer(String resourceServerId) { + ModelCriteriaBuilder mcb = policyStore.createCriteriaBuilder(); + + return resourceServerId == null + ? mcb + : mcb.compare(SearchableFields.RESOURCE_SERVER_ID, Operator.EQ, + resourceServerId); + } + + @Override + public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) { + LOG.tracef("create(%s, %s, %s)%s", representation.getId(), resourceServer.getId(), resourceServer, getShortStackTrace()); + + // @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"}) + ModelCriteriaBuilder mcb = forResourceServer(resourceServer.getId()) + .compare(SearchableFields.NAME, Operator.EQ, representation.getName()); + + if (tx.getCount(mcb) > 0) { + throw new ModelDuplicateException("Policy with name '" + representation.getName() + "' for " + resourceServer.getId() + " already exists"); + } + + UUID uid = representation.getId() == null ? UUID.randomUUID() : UUID.fromString(representation.getId()); + MapPolicyEntity entity = new MapPolicyEntity(uid); + entity.setType(representation.getType()); + entity.setName(representation.getName()); + entity.setResourceServerId(resourceServer.getId()); + + tx.create(uid, entity); + + return entityToAdapter(entity); + } + + @Override + public void delete(String id) { + LOG.tracef("delete(%s)%s", id, getShortStackTrace()); + tx.delete(UUID.fromString(id)); + } + + @Override + public Policy findById(String id, String resourceServerId) { + LOG.tracef("findById(%s, %s)%s", id, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.ID, Operator.EQ, id)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public Policy findByName(String name, String resourceServerId) { + LOG.tracef("findByName(%s, %s)%s", name, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.NAME, Operator.EQ, name)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public List findByResourceServer(String id) { + LOG.tracef("findByResourceServer(%s)%s", id, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(id)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + LOG.tracef("findByResource(%s, %s, %d, %d)%s", attributes, resourceServerId, firstResult, maxResult, getShortStackTrace()); + + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId).and( + attributes.entrySet().stream() + .map(this::filterEntryToModelCriteriaBuilder) + .toArray(ModelCriteriaBuilder[]::new) + ); + + if (!attributes.containsKey(Policy.FilterOption.OWNER) && !attributes.containsKey(Policy.FilterOption.OWNER_IS_NOT_NULL)) { + mcb = mcb.compare(SearchableFields.OWNER, Operator.NOT_EXISTS); + } + + return paginatedStream(tx.getUpdatedNotRemoved(mcb) + .sorted(AbstractPolicyEntity.COMPARE_BY_NAME), firstResult, maxResult) + .map(MapPolicyEntity::getId) + .map(UUID::toString) + .map(id -> authorizationProvider.getStoreFactory().getPolicyStore().findById(id, resourceServerId)) // We need to go through cache + .collect(Collectors.toList()); + } + + private ModelCriteriaBuilder filterEntryToModelCriteriaBuilder(Map.Entry entry) { + Policy.FilterOption name = entry.getKey(); + String[] value = entry.getValue(); + + switch (name) { + case ID: + case SCOPE_ID: + case RESOURCE_ID: + case OWNER: + return policyStore.createCriteriaBuilder() + .compare(name.getSearchableModelField(), Operator.IN, Arrays.asList(value)); + case PERMISSION: { + ModelCriteriaBuilder mcb = policyStore.createCriteriaBuilder() + .compare(SearchableFields.TYPE, Operator.IN, Arrays.asList("resource", "scope", "uma")); + + if (!Boolean.parseBoolean(value[0])) { + mcb = policyStore.createCriteriaBuilder().not(mcb); // TODO: create NOT_IN operator + } + + return mcb; + } + case OWNER_IS_NOT_NULL: + return policyStore.createCriteriaBuilder() + .compare(SearchableFields.OWNER, Operator.EXISTS); + case CONFIG: + if (value.length != 2) { + throw new IllegalArgumentException("Config filter option requires value with two items: [config_name, expected_config_value]"); + } + + value[1] = "%" + value[1] + "%"; + return policyStore.createCriteriaBuilder() + .compare(SearchableFields.CONFIG, Operator.LIKE, (Object[]) value); + case TYPE: + case NAME: + return policyStore.createCriteriaBuilder().compare(name.getSearchableModelField(), Operator.ILIKE, "%" + value[0] + "%"); + default: + throw new IllegalArgumentException("Unsupported filter [" + name + "]"); + + } + } + + @Override + public void findByResource(String resourceId, String resourceServerId, Consumer consumer) { + LOG.tracef("findByResource(%s, %s, %s)%s", resourceId, resourceServerId, consumer, getShortStackTrace()); + + tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(Policy.SearchableFields.RESOURCE_ID, Operator.EQ, resourceId)) + .map(this::entityToAdapter) + .forEach(consumer); + } + + @Override + public void findByResourceType(String type, String resourceServerId, Consumer policyConsumer) { + tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.CONFIG, Operator.LIKE, (Object[]) new String[] {"defaultResourceType", type})) + .map(this::entityToAdapter) + .forEach(policyConsumer); + } + + @Override + public List findByScopeIds(List scopeIds, String resourceServerId) { + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.SCOPE_ID, Operator.IN, scopeIds)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer) { + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId) + .compare(SearchableFields.TYPE, Operator.EQ, "scope") + .compare(SearchableFields.SCOPE_ID, Operator.IN, scopeIds); + + if (resourceId != null) { + mcb = mcb.compare(SearchableFields.RESOURCE_ID, Operator.EQ, resourceId); + // @NamedQuery(name="findPolicyIdByNullResourceScope", query="PolicyEntity pe left join fetch pe.config c inner join pe.scopes s where pe.resourceServer.id = :serverId and pe.type = 'scope' and pe.resources is empty and s.id in (:scopeIds) and not exists (select pec from pe.config pec where KEY(pec) = 'defaultResourceType')"), + } else { + mcb = mcb.compare(SearchableFields.RESOURCE_ID, Operator.NOT_EXISTS) + .compare(SearchableFields.CONFIG, Operator.NOT_EXISTS, (Object[]) new String[] {"defaultResourceType"}); + } + + tx.getUpdatedNotRemoved(mcb).map(this::entityToAdapter).forEach(consumer); + } + + @Override + public List findByType(String type, String resourceServerId) { + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.TYPE, Operator.EQ, type)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findDependentPolicies(String id, String resourceServerId) { + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.ASSOCIATED_POLICY_ID, Operator.EQ, id)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java new file mode 100644 index 00000000000..ef0a12002dc --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ModelException; +import org.keycloak.models.map.authorization.adapter.MapResourceServerAdapter; +import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.storage.StorageId; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; + +public class MapResourceServerStore implements ResourceServerStore { + + private static final Logger LOG = Logger.getLogger(MapResourceServerStore.class); + private final AuthorizationProvider authorizationProvider; + final MapKeycloakTransaction tx; + private final MapStorage resourceServerStore; + + public MapResourceServerStore(KeycloakSession session, MapStorage resourceServerStore, AuthorizationProvider provider) { + this.resourceServerStore = resourceServerStore; + this.tx = resourceServerStore.createTransaction(session); + this.authorizationProvider = provider; + session.getTransactionManager().enlist(tx); + } + + private MapResourceServerEntity registerEntityForChanges(MapResourceServerEntity origEntity) { + final MapResourceServerEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.updateIfChanged(origEntity.getId(), res, MapResourceServerEntity::isUpdated); + return res; + } + + private ResourceServer entityToAdapter(MapResourceServerEntity origEntity) { + if (origEntity == null) return null; + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return new MapResourceServerAdapter(registerEntityForChanges(origEntity), authorizationProvider.getStoreFactory()); + } + + @Override + public ResourceServer create(String clientId) { + LOG.tracef("create(%s)%s", clientId, getShortStackTrace()); + + if (clientId == null) return null; + + if (!StorageId.isLocalStorage(clientId)) { + throw new ModelException("Creating resource server from federated ClientModel not supported"); + } + + if (tx.read(clientId) != null) { + throw new ModelDuplicateException("Resource server already exists: " + clientId); + } + + MapResourceServerEntity entity = new MapResourceServerEntity(clientId); + + tx.create(entity.getId(), entity); + + return entityToAdapter(entity); + } + + @Override + public void delete(String id) { + LOG.tracef("delete(%s, %s)%s", id, getShortStackTrace()); + if (id == null) return; + + PolicyStore policyStore = authorizationProvider.getStoreFactory().getPolicyStore(); + policyStore.findByResourceServer(id).stream() + .map(Policy::getId) + .forEach(policyStore::delete); + + PermissionTicketStore permissionTicketStore = authorizationProvider.getStoreFactory().getPermissionTicketStore(); + permissionTicketStore.findByResourceServer(id).stream() + .map(PermissionTicket::getId) + .forEach(permissionTicketStore::delete); + + ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore(); + resourceStore.findByResourceServer(id).stream() + .map(Resource::getId) + .forEach(resourceStore::delete); + + ScopeStore scopeStore = authorizationProvider.getStoreFactory().getScopeStore(); + scopeStore.findByResourceServer(id).stream() + .map(Scope::getId) + .forEach(scopeStore::delete); + + tx.delete(id); + } + + @Override + public ResourceServer findById(String id) { + LOG.tracef("findById(%s)%s", id, getShortStackTrace()); + + if (id == null) { + return null; + } + + + MapResourceServerEntity entity = tx.read(id); + return entityToAdapter(entity); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java new file mode 100644 index 00000000000..4fbea321a89 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java @@ -0,0 +1,270 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.Resource.SearchableFields; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.authorization.adapter.MapResourceAdapter; +import org.keycloak.models.map.authorization.entity.AbstractResourceEntity; +import org.keycloak.models.map.authorization.entity.MapResourceEntity; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +public class MapResourceStore implements ResourceStore { + + private static final Logger LOG = Logger.getLogger(MapResourceStore.class); + private final AuthorizationProvider authorizationProvider; + final MapKeycloakTransaction tx; + private final MapStorage resourceStore; + + public MapResourceStore(KeycloakSession session, MapStorage resourceStore, AuthorizationProvider provider) { + this.resourceStore = resourceStore; + this.tx = resourceStore.createTransaction(session); + session.getTransactionManager().enlist(tx); + authorizationProvider = provider; + } + + private MapResourceEntity registerEntityForChanges(MapResourceEntity origEntity) { + final MapResourceEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.updateIfChanged(origEntity.getId(), res, MapResourceEntity::isUpdated); + return res; + } + + private Resource entityToAdapter(MapResourceEntity origEntity) { + if (origEntity == null) return null; + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return new MapResourceAdapter(registerEntityForChanges(origEntity), authorizationProvider.getStoreFactory()); + } + + private ModelCriteriaBuilder forResourceServer(String resourceServerId) { + ModelCriteriaBuilder mcb = resourceStore.createCriteriaBuilder(); + + return resourceServerId == null + ? mcb + : mcb.compare(SearchableFields.RESOURCE_SERVER_ID, Operator.EQ, + resourceServerId); + } + + @Override + public Resource create(String id, String name, ResourceServer resourceServer, String owner) { + LOG.tracef("create(%s, %s, %s, %s)%s", id, name, resourceServer, owner, getShortStackTrace()); + // @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID", "OWNER"}) + ModelCriteriaBuilder mcb = forResourceServer(resourceServer.getId()) + .compare(SearchableFields.NAME, Operator.EQ, name) + .compare(SearchableFields.OWNER, Operator.EQ, owner); + + if (tx.getCount(mcb) > 0) { + throw new ModelDuplicateException("Resource with name '" + name + "' for " + resourceServer.getId() + " already exists for request owner " + owner); + } + + UUID uid = id == null ? UUID.randomUUID() : UUID.fromString(id); + MapResourceEntity entity = new MapResourceEntity(uid); + + entity.setName(name); + entity.setResourceServerId(resourceServer.getId()); + entity.setOwner(owner); + + tx.create(uid, entity); + + return entityToAdapter(entity); + } + + @Override + public void delete(String id) { + LOG.tracef("delete(%s)%s", id, getShortStackTrace()); + + tx.delete(UUID.fromString(id)); + } + + @Override + public Resource findById(String id, String resourceServerId) { + LOG.tracef("findById(%s, %s)%s", id, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.ID, Operator.EQ, id)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public void findByOwner(String ownerId, String resourceServerId, Consumer consumer) { + findByOwnerFilter(ownerId, resourceServerId, consumer, -1, -1); + } + + private void findByOwnerFilter(String ownerId, String resourceServerId, Consumer consumer, int firstResult, int maxResult) { + LOG.tracef("findByOwnerFilter(%s, %s, %s, %d, %d)%s", ownerId, resourceServerId, consumer, firstResult, maxResult, getShortStackTrace()); + paginatedStream(tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.OWNER, Operator.EQ, ownerId)) + .sorted(MapResourceEntity.COMPARE_BY_ID), firstResult, maxResult) + .map(this::entityToAdapter) + .forEach(consumer); + } + + @Override + public List findByOwner(String ownerId, String resourceServerId, int first, int max) { + List resourceList = new LinkedList<>(); + + findByOwnerFilter(ownerId, resourceServerId, resourceList::add, first, max); + + return resourceList; + } + + @Override + public List findByUri(String uri, String resourceServerId) { + LOG.tracef("findByUri(%s, %s)%s", uri, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.URI, Operator.EQ, uri)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByResourceServer(String resourceServerId) { + LOG.tracef("findByResourceServer(%s)%s", resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + LOG.tracef("findByResourceServer(%s, %s, %d, %d)%s", attributes, resourceServerId, firstResult, maxResult, getShortStackTrace()); + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId).and( + attributes.entrySet().stream() + .map(this::filterEntryToModelCriteriaBuilder) + .toArray(ModelCriteriaBuilder[]::new) + ); + + return paginatedStream(tx.getUpdatedNotRemoved(mcb) + .sorted(AbstractResourceEntity.COMPARE_BY_NAME), firstResult, maxResult) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + private ModelCriteriaBuilder filterEntryToModelCriteriaBuilder(Map.Entry entry) { + Resource.FilterOption name = entry.getKey(); + String[] value = entry.getValue(); + + switch (name) { + case ID: + case SCOPE_ID: + case OWNER: + case URI: + return resourceStore.createCriteriaBuilder() + .compare(name.getSearchableModelField(), Operator.IN, Arrays.asList(value)); + case URI_NOT_NULL: + return resourceStore.createCriteriaBuilder().compare(SearchableFields.URI, Operator.EXISTS); + case OWNER_MANAGED_ACCESS: + return resourceStore.createCriteriaBuilder() + .compare(SearchableFields.OWNER_MANAGED_ACCESS, Operator.EQ, Boolean.valueOf(value[0])); + case EXACT_NAME: + return resourceStore.createCriteriaBuilder() + .compare(SearchableFields.NAME, Operator.EQ, value[0]); + case NAME: + return resourceStore.createCriteriaBuilder() + .compare(SearchableFields.NAME, Operator.ILIKE, "%" + value[0] + "%"); + default: + throw new IllegalArgumentException("Unsupported filter [" + name + "]"); + + } + } + + @Override + public void findByScope(List scopes, String resourceServerId, Consumer consumer) { + LOG.tracef("findByScope(%s, %s, %s)%s", scopes, resourceServerId, consumer, getShortStackTrace()); + + tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.SCOPE_ID, Operator.IN, scopes)) + .map(this::entityToAdapter) + .forEach(consumer); + } + + @Override + public Resource findByName(String name, String resourceServerId) { + return findByName(name, resourceServerId, resourceServerId); + } + + @Override + public Resource findByName(String name, String ownerId, String resourceServerId) { + LOG.tracef("findByName(%s, %s, %s)%s", name, ownerId, resourceServerId, getShortStackTrace()); + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.OWNER, Operator.EQ, ownerId) + .compare(SearchableFields.NAME, Operator.EQ, name)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public void findByType(String type, String resourceServerId, Consumer consumer) { + LOG.tracef("findByType(%s, %s, %s)%s", type, resourceServerId, consumer, getShortStackTrace()); + tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.TYPE, Operator.EQ, type)) + .map(this::entityToAdapter) + .forEach(consumer); + } + + @Override + public void findByType(String type, String owner, String resourceServerId, Consumer consumer) { + LOG.tracef("findByType(%s, %s, %s, %s)%s", type, owner, resourceServerId, consumer, getShortStackTrace()); + + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId) + .compare(SearchableFields.TYPE, Operator.EQ, type); + + if (owner != null) { + mcb = mcb.compare(SearchableFields.OWNER, Operator.EQ, owner); + } + + tx.getUpdatedNotRemoved(mcb) + .map(this::entityToAdapter) + .forEach(consumer); + } + + @Override + public void findByTypeInstance(String type, String resourceServerId, Consumer consumer) { + LOG.tracef("findByTypeInstance(%s, %s, %s)%s", type, resourceServerId, consumer, getShortStackTrace()); + tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(SearchableFields.OWNER, Operator.NE, resourceServerId) + .compare(SearchableFields.TYPE, Operator.EQ, type)) + .map(this::entityToAdapter) + .forEach(consumer); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java new file mode 100644 index 00000000000..feaf37813ce --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java @@ -0,0 +1,163 @@ +/* + * Copyright 2021 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.models.map.authorization; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.model.Scope.SearchableFields; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.authorization.adapter.MapScopeAdapter; +import org.keycloak.models.map.authorization.entity.MapScopeEntity; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +public class MapScopeStore implements ScopeStore { + + private static final Logger LOG = Logger.getLogger(MapScopeStore.class); + private final AuthorizationProvider authorizationProvider; + final MapKeycloakTransaction tx; + private final MapStorage scopeStore; + + public MapScopeStore(KeycloakSession session, MapStorage scopeStore, AuthorizationProvider provider) { + this.authorizationProvider = provider; + this.scopeStore = scopeStore; + this.tx = scopeStore.createTransaction(session); + session.getTransactionManager().enlist(tx); + } + + private MapScopeEntity registerEntityForChanges(MapScopeEntity origEntity) { + final MapScopeEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.updateIfChanged(origEntity.getId(), res, MapScopeEntity::isUpdated); + return res; + } + + private Scope entityToAdapter(MapScopeEntity origEntity) { + if (origEntity == null) return null; + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + return new MapScopeAdapter(registerEntityForChanges(origEntity), authorizationProvider.getStoreFactory()); + } + + private ModelCriteriaBuilder forResourceServer(String resourceServerId) { + ModelCriteriaBuilder mcb = scopeStore.createCriteriaBuilder(); + + return resourceServerId == null + ? mcb + : mcb.compare(SearchableFields.RESOURCE_SERVER_ID, Operator.EQ, + resourceServerId); + } + + @Override + public Scope create(String id, String name, ResourceServer resourceServer) { + LOG.tracef("create(%s, %s, %s)%s", id, name, resourceServer, getShortStackTrace()); + + + // @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"}) + ModelCriteriaBuilder mcb = forResourceServer(resourceServer.getId()) + .compare(SearchableFields.NAME, Operator.EQ, name); + + if (tx.getCount(mcb) > 0) { + throw new ModelDuplicateException("Scope with name '" + name + "' for " + resourceServer.getId() + " already exists"); + } + + UUID uid = id == null ? UUID.randomUUID() : UUID.fromString(id); + MapScopeEntity entity = new MapScopeEntity(uid); + + entity.setName(name); + entity.setResourceServerId(resourceServer.getId()); + + tx.create(uid, entity); + + return entityToAdapter(entity); + } + + @Override + public void delete(String id) { + LOG.tracef("delete(%s)%s", id, getShortStackTrace()); + tx.delete(UUID.fromString(id)); + } + + @Override + public Scope findById(String id, String resourceServerId) { + LOG.tracef("findById(%s, %s)%s", id, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId) + .compare(Scope.SearchableFields.ID, Operator.EQ, id)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public Scope findByName(String name, String resourceServerId) { + LOG.tracef("findByName(%s, %s)%s", name, resourceServerId, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(resourceServerId).compare(Scope.SearchableFields.NAME, + Operator.EQ, name)) + .findFirst() + .map(this::entityToAdapter) + .orElse(null); + } + + @Override + public List findByResourceServer(String id) { + LOG.tracef("findByResourceServer(%s)%s", id, getShortStackTrace()); + + return tx.getUpdatedNotRemoved(forResourceServer(id)) + .map(this::entityToAdapter) + .collect(Collectors.toList()); + } + + @Override + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + ModelCriteriaBuilder mcb = forResourceServer(resourceServerId); + + for (Scope.FilterOption filterOption : attributes.keySet()) { + String[] value = attributes.get(filterOption); + + switch (filterOption) { + case ID: + mcb = mcb.compare(Scope.SearchableFields.ID, Operator.IN, Arrays.asList(value)); + break; + case NAME: + mcb = mcb.compare(Scope.SearchableFields.NAME, Operator.ILIKE, "%" + value[0] + "%"); + break; + default: + throw new IllegalArgumentException("Unsupported filter [" + filterOption + "]"); + } + } + + return paginatedStream(tx.getUpdatedNotRemoved(mcb).map(this::entityToAdapter), firstResult, maxResult) + .collect(Collectors.toList()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPermissionTicketModel.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPermissionTicketModel.java new file mode 100644 index 00000000000..b27cdcc5e4f --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPermissionTicketModel.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractPermissionTicketModel implements PermissionTicket { + + protected final E entity; + protected final StoreFactory storeFactory; + + public AbstractPermissionTicketModel(E entity, StoreFactory storeFactory) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(storeFactory, "storeFactory"); + + this.storeFactory = storeFactory; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PermissionTicket)) return false; + + PermissionTicket that = (PermissionTicket) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPolicyModel.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPolicyModel.java new file mode 100644 index 00000000000..a46df6d98c1 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractPolicyModel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.AbstractAuthorizationModel; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractPolicyModel> extends AbstractAuthorizationModel implements Policy { + + protected final E entity; + + public AbstractPolicyModel(E entity, StoreFactory storeFactory) { + super(storeFactory); + + Objects.requireNonNull(entity, "entity"); + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Policy)) return false; + + Policy that = (Policy) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceModel.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceModel.java new file mode 100644 index 00000000000..fb3301bfb14 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceModel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.AbstractAuthorizationModel; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractResourceModel extends AbstractAuthorizationModel implements Resource { + + protected final E entity; + + public AbstractResourceModel(E entity, StoreFactory storeFactory) { + super(storeFactory); + + Objects.requireNonNull(entity, "entity"); + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Resource)) return false; + + Resource that = (Resource) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceServerModel.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceServerModel.java new file mode 100644 index 00000000000..5127647e37f --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractResourceServerModel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.AbstractAuthorizationModel; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractResourceServerModel extends AbstractAuthorizationModel implements ResourceServer { + + protected final E entity; + + public AbstractResourceServerModel(E entity, StoreFactory storeFactory) { + super(storeFactory); + + Objects.requireNonNull(entity, "entity"); + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ResourceServer)) return false; + + ResourceServer that = (ResourceServer) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractScopeModel.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractScopeModel.java new file mode 100644 index 00000000000..b7550cf6141 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/AbstractScopeModel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.AbstractAuthorizationModel; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractScopeModel extends AbstractAuthorizationModel implements Scope { + + protected final E entity; + + public AbstractScopeModel(E entity, StoreFactory storeFactory) { + super(storeFactory); + + Objects.requireNonNull(entity, "entity"); + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Scope)) return false; + + Scope that = (Scope) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPermissionTicketAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPermissionTicketAdapter.java new file mode 100644 index 00000000000..3f349ce2b4e --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPermissionTicketAdapter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; + +import java.util.UUID; + +import static org.keycloak.authorization.UserManagedPermissionUtil.updatePolicy; + +public class MapPermissionTicketAdapter extends AbstractPermissionTicketModel { + + public MapPermissionTicketAdapter(MapPermissionTicketEntity entity, StoreFactory storeFactory) { + super(entity, storeFactory); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public String getOwner() { + return entity.getOwner(); + } + + @Override + public String getRequester() { + return entity.getRequester(); + } + + @Override + public Resource getResource() { + return storeFactory.getResourceStore().findById(entity.getResourceId().toString(), entity.getResourceServerId()); + } + + @Override + public Scope getScope() { + if (entity.getScopeId() == null) return null; + return storeFactory.getScopeStore().findById(entity.getScopeId().toString(), entity.getResourceServerId()); + } + + @Override + public boolean isGranted() { + return entity.getGrantedTimestamp() != null; + } + + @Override + public Long getCreatedTimestamp() { + return entity.getCreatedTimestamp(); + } + + @Override + public Long getGrantedTimestamp() { + return entity.getGrantedTimestamp(); + } + + @Override + public void setGrantedTimestamp(Long millis) { + entity.setGrantedTimestamp(millis); + updatePolicy(this, storeFactory); + } + + @Override + public ResourceServer getResourceServer() { + return storeFactory.getResourceServerStore().findById(entity.getResourceServerId()); + } + + @Override + public Policy getPolicy() { + if (entity.getPolicyId() == null) return null; + return storeFactory.getPolicyStore().findById(entity.getPolicyId().toString(), entity.getResourceServerId()); + } + + @Override + public void setPolicy(Policy policy) { + if (policy != null) { + entity.setPolicyId(UUID.fromString(policy.getId())); + } + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPolicyAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPolicyAdapter.java new file mode 100644 index 00000000000..b13648aa8d0 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapPolicyAdapter.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.authorization.entity.MapPolicyEntity; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.Logic; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class MapPolicyAdapter extends AbstractPolicyModel { + + public MapPolicyAdapter(MapPolicyEntity entity, StoreFactory storeFactory) { + super(entity, storeFactory); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public String getType() { + return entity.getType(); + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return entity.getDecisionStrategy(); + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + throwExceptionIfReadonly(); + entity.setDecisionStrategy(decisionStrategy); + } + + @Override + public Logic getLogic() { + return entity.getLogic(); + } + + @Override + public void setLogic(Logic logic) { + throwExceptionIfReadonly(); + entity.setLogic(logic); + } + + @Override + public Map getConfig() { + return entity.getConfig(); + } + + @Override + public void setConfig(Map config) { + throwExceptionIfReadonly(); + entity.setConfig(config); + } + + @Override + public void removeConfig(String name) { + throwExceptionIfReadonly(); + entity.removeConfig(name); + } + + @Override + public void putConfig(String name, String value) { + throwExceptionIfReadonly(); + entity.putConfig(name, value); + } + + @Override + public String getName() { + return entity.getName(); + } + + @Override + public void setName(String name) { + throwExceptionIfReadonly(); + entity.setName(name); + } + + @Override + public String getDescription() { + return entity.getDescription(); + } + + @Override + public void setDescription(String description) { + throwExceptionIfReadonly(); + entity.setDescription(description); + } + + @Override + public ResourceServer getResourceServer() { + return storeFactory.getResourceServerStore().findById(entity.getResourceServerId()); + } + + @Override + public Set getAssociatedPolicies() { + String resourceServerId = entity.getResourceServerId(); + return entity.getAssociatedPoliciesIds().stream() + .map(policyId -> storeFactory.getPolicyStore().findById(policyId.toString(), resourceServerId)) + .collect(Collectors.toSet()); + } + + @Override + public Set getResources() { + String resourceServerId = entity.getResourceServerId(); + return entity.getResourceIds().stream() + .map(resourceId -> storeFactory.getResourceStore().findById(resourceId.toString(), resourceServerId)) + .collect(Collectors.toSet()); + } + + @Override + public Set getScopes() { + String resourceServerId = entity.getResourceServerId(); + return entity.getScopeIds().stream() + .map(scopeId -> storeFactory.getScopeStore().findById(scopeId.toString(), resourceServerId)) + .collect(Collectors.toSet()); + } + + @Override + public String getOwner() { + return entity.getOwner(); + } + + @Override + public void setOwner(String owner) { + throwExceptionIfReadonly(); + entity.setOwner(owner); + } + + @Override + public void addScope(Scope scope) { + throwExceptionIfReadonly(); + entity.addScope(UUID.fromString(scope.getId())); + } + + @Override + public void removeScope(Scope scope) { + throwExceptionIfReadonly(); + entity.removeScope(UUID.fromString(scope.getId())); + } + + @Override + public void addAssociatedPolicy(Policy associatedPolicy) { + throwExceptionIfReadonly(); + entity.addAssociatedPolicy(UUID.fromString(associatedPolicy.getId())); + } + + @Override + public void removeAssociatedPolicy(Policy associatedPolicy) { + throwExceptionIfReadonly(); + entity.removeAssociatedPolicy(UUID.fromString(associatedPolicy.getId())); + } + + @Override + public void addResource(Resource resource) { + throwExceptionIfReadonly(); + entity.addResource(UUID.fromString(resource.getId())); + } + + @Override + public void removeResource(Resource resource) { + throwExceptionIfReadonly(); + entity.removeResource(UUID.fromString(resource.getId())); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceAdapter.java new file mode 100644 index 00000000000..21987877bb6 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceAdapter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.authorization.entity.MapResourceEntity; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class MapResourceAdapter extends AbstractResourceModel { + + public MapResourceAdapter(MapResourceEntity entity, StoreFactory storeFactory) { + super(entity, storeFactory); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public String getName() { + return entity.getName(); + } + + @Override + public void setName(String name) { + throwExceptionIfReadonly(); + entity.setName(name); + } + + @Override + public String getDisplayName() { + return entity.getDisplayName(); + } + + @Override + public void setDisplayName(String name) { + throwExceptionIfReadonly(); + entity.setDisplayName(name); + } + + @Override + public Set getUris() { + return entity.getUris(); + } + + @Override + public void updateUris(Set uri) { + throwExceptionIfReadonly(); + entity.setUris(uri); + } + + @Override + public String getType() { + return entity.getType(); + } + + @Override + public void setType(String type) { + throwExceptionIfReadonly(); + entity.setType(type); + } + + @Override + public List getScopes() { + return entity.getScopeIds().stream() + .map(id -> storeFactory + .getScopeStore().findById(id.toString(), entity.getResourceServerId())) + .collect(Collectors.toList()); + } + + @Override + public String getIconUri() { + return entity.getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + throwExceptionIfReadonly(); + entity.setIconUri(iconUri); + } + + @Override + public String getResourceServer() { + return entity.getResourceServerId(); + } + + @Override + public String getOwner() { + return entity.getOwner(); + } + + @Override + public boolean isOwnerManagedAccess() { + return entity.isOwnerManagedAccess(); + } + + @Override + public void setOwnerManagedAccess(boolean ownerManagedAccess) { + throwExceptionIfReadonly(); + entity.setOwnerManagedAccess(ownerManagedAccess); + } + + @Override + public void updateScopes(Set scopes) { + throwExceptionIfReadonly(); + entity.setScopeIds(scopes.stream().map(Scope::getId).map(UUID::fromString).collect(Collectors.toSet())); + } + + @Override + public Map> getAttributes() { + return Collections.unmodifiableMap(new HashMap<>(entity.getAttributes())); + } + + @Override + public String getSingleAttribute(String name) { + return entity.getSingleAttribute(name); + } + + @Override + public List getAttribute(String name) { + return entity.getAttribute(name); + } + + @Override + public void setAttribute(String name, List values) { + throwExceptionIfReadonly(); + entity.setAttribute(name, values); + } + + @Override + public void removeAttribute(String name) { + throwExceptionIfReadonly(); + entity.removeAttribute(name); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceServerAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceServerAdapter.java new file mode 100644 index 00000000000..7aa5b8932c1 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapResourceServerAdapter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + + +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; + +public class MapResourceServerAdapter extends AbstractResourceServerModel { + + public MapResourceServerAdapter(MapResourceServerEntity entity, StoreFactory storeFactory) { + super(entity, storeFactory); + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public boolean isAllowRemoteResourceManagement() { + return entity.isAllowRemoteResourceManagement(); + } + + @Override + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + throwExceptionIfReadonly(); + entity.setAllowRemoteResourceManagement(allowRemoteResourceManagement); + } + + @Override + public PolicyEnforcementMode getPolicyEnforcementMode() { + return entity.getPolicyEnforcementMode(); + } + + @Override + public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) { + throwExceptionIfReadonly(); + entity.setPolicyEnforcementMode(enforcementMode); + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + throwExceptionIfReadonly(); + entity.setDecisionStrategy(decisionStrategy); + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return entity.getDecisionStrategy(); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapScopeAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapScopeAdapter.java new file mode 100644 index 00000000000..21080939d93 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/adapter/MapScopeAdapter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 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.models.map.authorization.adapter; + + +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.map.authorization.entity.MapScopeEntity; + +public class MapScopeAdapter extends AbstractScopeModel { + + public MapScopeAdapter(MapScopeEntity entity, StoreFactory storeFactory) { + super(entity, storeFactory); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public String getName() { + return entity.getName(); + } + + @Override + public void setName(String name) { + throwExceptionIfReadonly(); + entity.setName(name); + } + + @Override + public String getDisplayName() { + return entity.getDisplayName(); + } + + @Override + public void setDisplayName(String name) { + throwExceptionIfReadonly(); + entity.setDisplayName(name); + } + + @Override + public String getIconUri() { + return entity.getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + throwExceptionIfReadonly(); + entity.setIconUri(iconUri); + } + + @Override + public ResourceServer getResourceServer() { + return storeFactory.getResourceServerStore().findById(entity.getResourceServerId()); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPermissionTicketEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPermissionTicketEntity.java new file mode 100644 index 00000000000..9f9c4afaa10 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPermissionTicketEntity.java @@ -0,0 +1,127 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractPermissionTicketEntity implements AbstractEntity { + + private final K id; + private String owner; + private String requester; + private Long createdTimestamp; + private Long grantedTimestamp; + private K resourceId; + private K scopeId; + private String resourceServerId; + private K policyId; + private boolean updated = false; + + protected AbstractPermissionTicketEntity(K id) { + this.id = id; + } + + public AbstractPermissionTicketEntity() { + this.id = null; + } + + @Override + public K getId() { + return id; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.updated |= !Objects.equals(this.owner, owner); + this.owner = owner; + } + + public String getRequester() { + return requester; + } + + public void setRequester(String requester) { + this.updated |= !Objects.equals(this.requester, requester); + this.requester = requester; + } + + public Long getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(Long createdTimestamp) { + this.updated |= !Objects.equals(this.createdTimestamp, createdTimestamp); + this.createdTimestamp = createdTimestamp; + } + + public Long getGrantedTimestamp() { + return grantedTimestamp; + } + + public void setGrantedTimestamp(Long grantedTimestamp) { + this.updated |= !Objects.equals(this.grantedTimestamp, grantedTimestamp); + this.grantedTimestamp = grantedTimestamp; + } + + public K getResourceId() { + return resourceId; + } + + public void setResourceId(K resourceId) { + this.updated |= !Objects.equals(this.resourceId, resourceId); + this.resourceId = resourceId; + } + + public K getScopeId() { + return scopeId; + } + + public void setScopeId(K scopeId) { + this.updated |= !Objects.equals(this.scopeId, scopeId); + this.scopeId = scopeId; + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.updated |= !Objects.equals(this.resourceServerId, resourceServerId); + this.resourceServerId = resourceServerId; + } + + public K getPolicyId() { + return policyId; + } + + public void setPolicyId(K policyId) { + this.updated |= !Objects.equals(this.policyId, policyId); + this.policyId = policyId; + } + + @Override + public boolean isUpdated() { + return updated; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPolicyEntity.java new file mode 100644 index 00000000000..e75afcb6119 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractPolicyEntity.java @@ -0,0 +1,191 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.Logic; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.Map; +import java.util.Objects; + +public abstract class AbstractPolicyEntity implements AbstractEntity { + + public static final Comparator> COMPARE_BY_NAME = Comparator.comparing(AbstractPolicyEntity::getName); + + private final K id; + private String name; + private String description; + private String type; + private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + private Logic logic = Logic.POSITIVE; + private final Map config = new HashMap<>(); + private String resourceServerId; + private final Set associatedPoliciesIds = new HashSet<>(); + private final Set resourceIds = new HashSet<>(); + private final Set scopeIds = new HashSet<>(); + private String owner; + private boolean updated = false; + + protected AbstractPolicyEntity(K id) { + this.id = id; + } + + public AbstractPolicyEntity() { + this.id = null; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.updated |= !Objects.equals(this.name, name); + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.updated |= !Objects.equals(this.description, description); + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.updated |= !Objects.equals(this.type, type); + this.type = type; + } + + public DecisionStrategy getDecisionStrategy() { + return decisionStrategy; + } + + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.updated |= !Objects.equals(this.decisionStrategy, decisionStrategy); + this.decisionStrategy = decisionStrategy; + } + + public Logic getLogic() { + return logic; + } + + public void setLogic(Logic logic) { + this.updated |= !Objects.equals(this.logic, logic); + this.logic = logic; + } + + public Map getConfig() { + return config; + } + + public String getConfigValue(String name) { + return config.get(name); + } + + public void setConfig(Map config) { + if (Objects.equals(this.config, config)) return; + + this.updated = true; + this.config.clear(); + if (config != null) { + this.config.putAll(config); + } + } + + public void removeConfig(String name) { + this.updated |= this.config.remove(name) != null; + } + + public void putConfig(String name, String value) { + this.updated |= !Objects.equals(value, this.config.put(name, value)); + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.updated |= !Objects.equals(this.resourceServerId, resourceServerId); + this.resourceServerId = resourceServerId; + } + + public Set getAssociatedPoliciesIds() { + return associatedPoliciesIds; + } + + public void addAssociatedPolicy(K policyId) { + this.updated |= this.associatedPoliciesIds.add(policyId); + } + + public void removeAssociatedPolicy(K policyId) { + this.updated |= this.associatedPoliciesIds.remove(policyId); + } + + public Set getResourceIds() { + return resourceIds; + } + + public void addResource(K resourceId) { + this.updated |= this.resourceIds.add(resourceId); + } + + public void removeResource(K resourceId) { + this.updated |= this.resourceIds.remove(resourceId); + } + + public Set getScopeIds() { + return scopeIds; + } + + public void addScope(K scopeId) { + this.updated |= this.scopeIds.add(scopeId); + } + + public void removeScope(K scopeId) { + this.updated |= this.scopeIds.remove(scopeId); + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.updated |= !Objects.equals(this.owner, owner); + this.owner = owner; + } + + @Override + public K getId() { + return id; + } + + @Override + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceEntity.java new file mode 100644 index 00000000000..be8f19366e5 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceEntity.java @@ -0,0 +1,183 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public abstract class AbstractResourceEntity implements AbstractEntity { + + public static final Comparator> COMPARE_BY_NAME = Comparator.comparing(AbstractResourceEntity::getName); + + private final K id; + private String name; + private String displayName; + private final Set uris = new HashSet<>(); + private String type; + private String iconUri; + private String owner; + private boolean ownerManagedAccess; + private String resourceServerId; + private final Set scopeIds = new HashSet<>(); + private final Set policyIds = new HashSet<>(); + private final Map> attributes = new HashMap<>(); + private boolean updated = false; + + protected AbstractResourceEntity(K id) { + this.id = id; + } + + public AbstractResourceEntity() { + this.id = null; + } + + @Override + public K getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.updated |= !Objects.equals(this.name, name); + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.updated |= !Objects.equals(this.displayName, displayName); + this.displayName = displayName; + } + + public Set getUris() { + return uris; + } + + public void setUris(Set uris) { + if (Objects.equals(this.uris, uris)) return; + + this.updated = true; + this.uris.clear(); + + if (uris != null) { + this.uris.addAll(uris); + } + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.updated |= !Objects.equals(this.type, type); + this.type = type; + } + + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.updated |= !Objects.equals(this.iconUri, iconUri); + this.iconUri = iconUri; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.updated |= !Objects.equals(this.owner, owner); + this.owner = owner; + } + + public boolean isOwnerManagedAccess() { + return ownerManagedAccess; + } + + public void setOwnerManagedAccess(boolean ownerManagedAccess) { + this.updated |= this.ownerManagedAccess != ownerManagedAccess; + this.ownerManagedAccess = ownerManagedAccess; + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.updated |= !Objects.equals(this.resourceServerId, resourceServerId); + this.resourceServerId = resourceServerId; + } + + public Set getScopeIds() { + return scopeIds; + } + + public void setScopeIds(Set scopeIds) { + if (Objects.equals(this.scopeIds, scopeIds)) return; + + this.updated = true; + this.scopeIds.clear(); + if (scopeIds != null) { + this.scopeIds.addAll(scopeIds); + } + } + + public Set getPolicyIds() { + return policyIds; + } + + public Map> getAttributes() { + return attributes; + } + + public List getAttribute(String name) { + return attributes.get(name); + } + + public String getSingleAttribute(String name) { + List attributeValues = attributes.get(name); + return attributeValues == null || attributeValues.isEmpty() ? null : attributeValues.get(0); + } + + public void setAttribute(String name, List value) { + this.updated |= !Objects.equals(this.attributes.put(name, value), value); + } + + public void removeAttribute(String name) { + this.updated |= this.attributes.remove(name) != null; + } + + @Override + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceServerEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceServerEntity.java new file mode 100644 index 00000000000..f8bbacb8978 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractResourceServerEntity.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; + +import java.util.Objects; + +public abstract class AbstractResourceServerEntity implements AbstractEntity { + + private final K id; + private boolean updated = false; + + private boolean allowRemoteResourceManagement; + private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING; + private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + + protected AbstractResourceServerEntity(K id) { + this.id = id; + } + + public AbstractResourceServerEntity() { + this.id = null; + } + + @Override + public K getId() { + return id; + } + + public boolean isAllowRemoteResourceManagement() { + return allowRemoteResourceManagement; + } + + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + this.updated |= this.allowRemoteResourceManagement != allowRemoteResourceManagement; + this.allowRemoteResourceManagement = allowRemoteResourceManagement; + } + + public PolicyEnforcementMode getPolicyEnforcementMode() { + return policyEnforcementMode; + } + + public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) { + this.updated |= !Objects.equals(this.policyEnforcementMode, policyEnforcementMode); + this.policyEnforcementMode = policyEnforcementMode; + } + + public DecisionStrategy getDecisionStrategy() { + return decisionStrategy; + } + + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.updated |= !Objects.equals(this.decisionStrategy, decisionStrategy); + this.decisionStrategy = decisionStrategy; + } + + @Override + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractScopeEntity.java new file mode 100644 index 00000000000..cfcb202ce67 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/AbstractScopeEntity.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractScopeEntity implements AbstractEntity { + + private final K id; + private String name; + private String displayName; + private String iconUri; + private String resourceServerId; + private boolean updated = false; + + protected AbstractScopeEntity(K id) { + this.id = id; + } + + public AbstractScopeEntity() { + this.id = null; + } + + @Override + public K getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.updated |= !Objects.equals(this.name, name); + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.updated |= !Objects.equals(this.displayName, displayName); + this.displayName = displayName; + } + + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.updated |= !Objects.equals(this.iconUri, iconUri); + this.iconUri = iconUri; + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.updated |= !Objects.equals(this.resourceServerId, resourceServerId); + this.resourceServerId = resourceServerId; + } + + @Override + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java new file mode 100644 index 00000000000..8932d5273ec --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import java.util.Comparator; +import java.util.UUID; + +public class MapPermissionTicketEntity extends AbstractPermissionTicketEntity { + + public static final Comparator> COMPARE_BY_ID = Comparator.comparing(AbstractPermissionTicketEntity::getId); + public static final Comparator> COMPARE_BY_RESOURCE_ID = Comparator.comparing(AbstractPermissionTicketEntity::getResourceId); + + + protected MapPermissionTicketEntity() { + super(); + } + + public MapPermissionTicketEntity(UUID id) { + super(id); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java new file mode 100644 index 00000000000..240d8a76440 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import java.util.UUID; + +public class MapPolicyEntity extends AbstractPolicyEntity { + protected MapPolicyEntity() { + super(); + } + + public MapPolicyEntity(UUID id) { + super(id); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java new file mode 100644 index 00000000000..c41c65ee38b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import java.util.Comparator; +import java.util.UUID; + +public class MapResourceEntity extends AbstractResourceEntity { + public static final Comparator> COMPARE_BY_ID = Comparator.comparing(AbstractResourceEntity::getId); + + protected MapResourceEntity() { + super(); + } + + public MapResourceEntity(UUID id) { + super(id); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java new file mode 100644 index 00000000000..a78ca804c4c --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +public class MapResourceServerEntity extends AbstractResourceServerEntity { + protected MapResourceServerEntity() { + super(); + } + + public MapResourceServerEntity(String id) { + super(id); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java new file mode 100644 index 00000000000..52776ae4399 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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.models.map.authorization.entity; + +import java.util.UUID; + +public class MapScopeEntity extends AbstractScopeEntity { + protected MapScopeEntity() { + super(); + } + + public MapScopeEntity(UUID id) { + super(id); + } + + @Override + public String toString() { + return String.format("%s@%08x", getId(), System.identityHashCode(this)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java index 73387f3f5f9..01d5c418861 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java @@ -16,6 +16,11 @@ */ package org.keycloak.models.map.storage; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.GroupModel; @@ -23,6 +28,11 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.map.authSession.AbstractRootAuthenticationSessionEntity; +import org.keycloak.models.map.authorization.entity.AbstractPermissionTicketEntity; +import org.keycloak.models.map.authorization.entity.AbstractPolicyEntity; +import org.keycloak.models.map.authorization.entity.AbstractResourceEntity; +import org.keycloak.models.map.authorization.entity.AbstractResourceServerEntity; +import org.keycloak.models.map.authorization.entity.AbstractScopeEntity; import org.keycloak.models.map.client.AbstractClientEntity; import org.keycloak.models.map.clientscope.AbstractClientScopeEntity; import org.keycloak.models.map.common.AbstractEntity; @@ -58,6 +68,11 @@ public class MapFieldPredicates { public static final Map, UpdatePredicatesFunc, RoleModel>> ROLE_PREDICATES = basePredicates(RoleModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, UserModel>> USER_PREDICATES = basePredicates(UserModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, RootAuthenticationSessionModel>> AUTHENTICATION_SESSION_PREDICATES = basePredicates(RootAuthenticationSessionModel.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, ResourceServer>> AUTHZ_RESOURCE_SERVER_PREDICATES = basePredicates(ResourceServer.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, Resource>> AUTHZ_RESOURCE_PREDICATES = basePredicates(Resource.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, Scope>> AUTHZ_SCOPE_PREDICATES = basePredicates(Scope.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, PermissionTicket>> AUTHZ_PERMISSION_TICKET_PREDICATES = basePredicates(PermissionTicket.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, Policy>> AUTHZ_POLICY_PREDICATES = basePredicates(Policy.SearchableFields.ID); @SuppressWarnings("unchecked") private static final Map, Map> PREDICATES = new HashMap<>(); @@ -105,6 +120,40 @@ public class MapFieldPredicates { put(AUTHENTICATION_SESSION_PREDICATES, RootAuthenticationSessionModel.SearchableFields.REALM_ID, AbstractRootAuthenticationSessionEntity::getRealmId); put(AUTHENTICATION_SESSION_PREDICATES, RootAuthenticationSessionModel.SearchableFields.TIMESTAMP, AbstractRootAuthenticationSessionEntity::getTimestamp); + + put(AUTHZ_RESOURCE_SERVER_PREDICATES, ResourceServer.SearchableFields.ID, AbstractResourceServerEntity::getId); + + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.ID, predicateForKeyField(AbstractResourceEntity::getId)); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.NAME, AbstractResourceEntity::getName); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.RESOURCE_SERVER_ID, AbstractResourceEntity::getResourceServerId); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.OWNER, AbstractResourceEntity::getOwner); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.TYPE, AbstractResourceEntity::getType); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.URI, MapFieldPredicates::checkResourceUri); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.SCOPE_ID, MapFieldPredicates::checkResourceScopes); + put(AUTHZ_RESOURCE_PREDICATES, Resource.SearchableFields.OWNER_MANAGED_ACCESS, AbstractResourceEntity::isOwnerManagedAccess); + + put(AUTHZ_SCOPE_PREDICATES, Scope.SearchableFields.ID, predicateForKeyField(AbstractScopeEntity::getId)); + put(AUTHZ_SCOPE_PREDICATES, Scope.SearchableFields.RESOURCE_SERVER_ID, AbstractScopeEntity::getResourceServerId); + put(AUTHZ_SCOPE_PREDICATES, Scope.SearchableFields.NAME, AbstractScopeEntity::getName); + + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.ID, predicateForKeyField(AbstractPermissionTicketEntity::getId)); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.OWNER, AbstractPermissionTicketEntity::getOwner); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.REQUESTER, AbstractPermissionTicketEntity::getRequester); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.RESOURCE_SERVER_ID, AbstractPermissionTicketEntity::getResourceServerId); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.RESOURCE_ID, predicateForKeyField(AbstractPermissionTicketEntity::getResourceId)); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.SCOPE_ID, predicateForKeyField(AbstractPermissionTicketEntity::getScopeId)); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.POLICY_ID, predicateForKeyField(AbstractPermissionTicketEntity::getPolicyId)); + put(AUTHZ_PERMISSION_TICKET_PREDICATES, PermissionTicket.SearchableFields.GRANTED_TIMESTAMP, AbstractPermissionTicketEntity::getGrantedTimestamp); + + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.ID, predicateForKeyField(AbstractPolicyEntity::getId)); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.NAME, AbstractPolicyEntity::getName); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.OWNER, AbstractPolicyEntity::getOwner); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.TYPE, AbstractPolicyEntity::getType); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.RESOURCE_SERVER_ID, AbstractPolicyEntity::getResourceServerId); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.RESOURCE_ID, MapFieldPredicates::checkPolicyResources); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.SCOPE_ID, MapFieldPredicates::checkPolicyScopes); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.CONFIG, MapFieldPredicates::checkPolicyConfig); + put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.ASSOCIATED_POLICY_ID, MapFieldPredicates::checkAssociatedPolicy); } static { @@ -115,6 +164,11 @@ public class MapFieldPredicates { PREDICATES.put(GroupModel.class, GROUP_PREDICATES); PREDICATES.put(UserModel.class, USER_PREDICATES); PREDICATES.put(RootAuthenticationSessionModel.class, AUTHENTICATION_SESSION_PREDICATES); + PREDICATES.put(ResourceServer.class, AUTHZ_RESOURCE_SERVER_PREDICATES); + PREDICATES.put(Resource.class, AUTHZ_RESOURCE_PREDICATES); + PREDICATES.put(Scope.class, AUTHZ_SCOPE_PREDICATES); + PREDICATES.put(PermissionTicket.class, AUTHZ_PERMISSION_TICKET_PREDICATES); + PREDICATES.put(Policy.class, AUTHZ_POLICY_PREDICATES); } private static , M> void put( @@ -128,8 +182,19 @@ public class MapFieldPredicates { SearchableModelField field, UpdatePredicatesFunc function) { map.put(field, function); } + + private static > Function predicateForKeyField(Function extractor) { + return entity -> { + Object o = extractor.apply(entity); + return o == null ? null : o.toString(); + }; + } private static String ensureEqSingleValue(SearchableModelField field, String parameterName, Operator op, Object[] values) throws CriterionNotSupportedException { + return ensureEqSingleValue(field, parameterName, op, values, String.class); + } + + private static T ensureEqSingleValue(SearchableModelField field, String parameterName, Operator op, Object[] values, Class expectedType) throws CriterionNotSupportedException { if (op != Operator.EQ) { throw new CriterionNotSupportedException(field, op); } @@ -138,11 +203,11 @@ public class MapFieldPredicates { } final Object ob = values[0]; - if (! (ob instanceof String)) { - throw new CriterionNotSupportedException(field, op, "Invalid arguments, expected (String role_id), got: " + Arrays.toString(values)); + if (!expectedType.isAssignableFrom(ob.getClass())) { + throw new CriterionNotSupportedException(field, op, "Invalid arguments, expected (" + expectedType.getName() + "), got: " + Arrays.toString(values)); } - String s = (String) ob; - return s; + + return expectedType.cast(ob); } private static MapModelCriteriaBuilder, ClientModel> checkScopeMappingRole(MapModelCriteriaBuilder, ClientModel> mcb, Operator op, Object[] values) { @@ -198,6 +263,100 @@ public class MapFieldPredicates { return mcb.fieldCompare(Boolean.TRUE::equals, getter); } + private static MapModelCriteriaBuilder, Resource> checkResourceUri(MapModelCriteriaBuilder, Resource> mcb, Operator op, Object[] values) { + Function, ?> getter; + + if (Operator.EXISTS.equals(op)) { + getter = re -> re.getUris() != null && !re.getUris().isEmpty(); + } else if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { + Collection c = (Collection) values[0]; + getter = re -> re.getUris().stream().anyMatch(c::contains); + } else { + String uri = ensureEqSingleValue(Resource.SearchableFields.URI, "uri", op, values); + getter = re -> re.getUris().contains(uri); + } + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, Resource> checkResourceScopes(MapModelCriteriaBuilder, Resource> mcb, Operator op, Object[] values) { + Function, ?> getter; + + if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { + Collection c = (Collection) values[0]; + getter = re -> re.getScopeIds().stream().map(Object::toString).anyMatch(c::contains); + } else { + String scope = ensureEqSingleValue(Resource.SearchableFields.URI, "scope_id", op, values); + getter = re -> re.getScopeIds().stream().map(Object::toString).anyMatch(scope::equals); + } + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, Policy> checkPolicyResources(MapModelCriteriaBuilder, Policy> mcb, Operator op, Object[] values) { + Function, ?> getter; + + if (op == Operator.NOT_EXISTS) { + getter = re -> re.getResourceIds().isEmpty(); + } else if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { + Collection c = (Collection) values[0]; + getter = re -> re.getResourceIds().stream().map(Object::toString).anyMatch(c::contains); + } else { + String scope = ensureEqSingleValue(Policy.SearchableFields.RESOURCE_ID, "resource_id", op, values, String.class); + getter = re -> re.getResourceIds().stream().map(Object::toString).anyMatch(scope::equals); + } + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, Policy> checkPolicyScopes(MapModelCriteriaBuilder, Policy> mcb, Operator op, Object[] values) { + Function, ?> getter; + + if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { + Collection c = (Collection) values[0]; + getter = re -> re.getScopeIds().stream().map(Object::toString).anyMatch(c::contains); // TODO: Use KeyConverter + } else { + String scope = ensureEqSingleValue(Policy.SearchableFields.CONFIG, "scope_id", op, values); + getter = re -> re.getScopeIds().stream().map(Object::toString).anyMatch(scope::equals); + } + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, Policy> checkPolicyConfig(MapModelCriteriaBuilder, Policy> mcb, Operator op, Object[] values) { + Function, ?> getter; + + final Object attrName = values[0]; + if (!(attrName instanceof String)) { + throw new CriterionNotSupportedException(UserModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected (String attribute_name), got: " + Arrays.toString(values)); + } + + String attrNameS = (String) attrName; + Object[] realValues = new Object[values.length - 1]; + System.arraycopy(values, 1, realValues, 0, values.length - 1); + Predicate valueComparator = CriteriaOperator.predicateFor(op, realValues); + getter = pe -> { + final String configValue = pe.getConfigValue(attrNameS); + return valueComparator.test(configValue); + }; + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + + private static MapModelCriteriaBuilder, Policy> checkAssociatedPolicy(MapModelCriteriaBuilder, Policy> mcb, Operator op, Object[] values) { + Function, ?> getter; + + if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { + Collection c = (Collection) values[0]; + getter = re -> re.getAssociatedPoliciesIds().stream().map(Object::toString).anyMatch(c::contains); + } else { + String policyId = ensureEqSingleValue(Policy.SearchableFields.ASSOCIATED_POLICY_ID, "associated_policy_id", op, values); + getter = re -> re.getAssociatedPoliciesIds().stream().map(Object::toString).anyMatch(policyId::equals); + } + + return mcb.fieldCompare(Boolean.TRUE::equals, getter); + } + private static MapModelCriteriaBuilder, UserModel> checkUserGroup(MapModelCriteriaBuilder, UserModel> mcb, Operator op, Object[] values) { Function, ?> getter; if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapModelCriteriaBuilder.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapModelCriteriaBuilder.java index 78f91a665d9..bb3a8ff6671 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapModelCriteriaBuilder.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapModelCriteriaBuilder.java @@ -91,10 +91,11 @@ public class MapModelCriteriaBuilder, M> implemen } Predicate resIndexFilter = b.getKeyFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : b.getKeyFilter().negate(); Predicate resEntityFilter = b.getEntityFilter() == ALWAYS_TRUE ? ALWAYS_TRUE : b.getEntityFilter().negate(); + return new MapModelCriteriaBuilder<>( fieldPredicates, - v -> keyFilter.test(v) && ! resIndexFilter.test(v), - v -> entityFilter.test(v) && ! resEntityFilter.test(v) + v -> keyFilter.test(v) && resIndexFilter.test(v), + v -> entityFilter.test(v) && resEntityFilter.test(v) ); } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index c8663b11093..608ef154c2e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -50,6 +50,7 @@ import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.client.ClientStorageProvider; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -691,9 +692,8 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor HashSet authorizedGroups = new HashSet<>(userGroups); authorizedGroups.removeIf(id -> { - Map values = new HashMap<>(); - values.put(Resource.EXACT_NAME, new String[] { "true" }); - values.put("name", new String[] { "group.resource." + id }); + Map values = new EnumMap<>(Resource.FilterOption.class); + values.put(Resource.FilterOption.EXACT_NAME, new String[] { "group.resource." + id }); return resourceStore.findByResourceServer(values, null, 0, 1).isEmpty(); }); diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory new file mode 100644 index 00000000000..b68658f9936 --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2021 Red Hat, Inc., and individual contributors +# as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.models.map.authorization.MapAuthorizationStoreFactory 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 cde1d47ac72..52a0bcadd60 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 @@ -280,7 +280,7 @@ public final class AuthorizationProvider implements Provider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return delegate.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } }; @@ -385,7 +385,7 @@ public final class AuthorizationProvider implements Provider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return policyStore.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } @@ -506,7 +506,7 @@ public final class AuthorizationProvider implements Provider { } @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { return delegate.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); } 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 index 711d4d7dc9a..8fbc7f1a09e 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/UserManagedPermissionUtil.java @@ -16,8 +16,9 @@ */ package org.keycloak.authorization; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.Policy; @@ -38,12 +39,12 @@ public class UserManagedPermissionUtil { Policy policy = ticket.getPolicy(); if (policy == null) { - HashMap filter = new HashMap<>(); + Map filter = new EnumMap<>(PermissionTicket.FilterOption.class); - 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()); + filter.put(PermissionTicket.FilterOption.OWNER, ticket.getOwner()); + filter.put(PermissionTicket.FilterOption.REQUESTER, ticket.getRequester()); + filter.put(PermissionTicket.FilterOption.RESOURCE_ID, ticket.getResource().getId()); + filter.put(PermissionTicket.FilterOption.POLICY_IS_NOT_NULL, Boolean.TRUE.toString()); List tickets = storeFactory.getPermissionTicketStore().find(filter, ticket.getResourceServer().getId(), -1, 1); @@ -72,12 +73,12 @@ public class UserManagedPermissionUtil { Policy policy = ticket.getPolicy(); if (policy != null) { - HashMap filter = new HashMap<>(); + Map filter = new EnumMap<>(PermissionTicket.FilterOption.class); - 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()); + filter.put(PermissionTicket.FilterOption.OWNER, ticket.getOwner()); + filter.put(PermissionTicket.FilterOption.REQUESTER, ticket.getRequester()); + filter.put(PermissionTicket.FilterOption.RESOURCE_ID, ticket.getResource().getId()); + filter.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); List tickets = storeFactory.getPermissionTicketStore().find(filter, ticket.getResourceServer().getId(), -1, -1); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/AbstractAuthorizationModel.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/AbstractAuthorizationModel.java index 2f35aeaa39e..e8982a47afb 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/AbstractAuthorizationModel.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/AbstractAuthorizationModel.java @@ -18,11 +18,14 @@ package org.keycloak.authorization.model; import org.keycloak.authorization.store.StoreFactory; +import java.util.Objects; + public abstract class AbstractAuthorizationModel { - private StoreFactory storeFactory; + protected final StoreFactory storeFactory; public AbstractAuthorizationModel(StoreFactory storeFactory) { + Objects.requireNonNull(storeFactory, "storeFactory"); this.storeFactory = storeFactory; } 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 035c8007a1c..88df34966b5 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 @@ -16,22 +16,55 @@ */ package org.keycloak.authorization.model; +import org.keycloak.storage.SearchableModelField; + /** * @author Pedro Igor */ public interface PermissionTicket { - String ID = "id"; - String RESOURCE = "resource.id"; - String RESOURCE_NAME = "resource.name"; - String SCOPE = "scope.id"; - String SCOPE_IS_NULL = "scope_is_null"; - String OWNER = "owner"; - String GRANTED = "granted"; - String REQUESTER = "requester"; - String REQUESTER_IS_NULL = "requester_is_null"; - String POLICY_IS_NOT_NULL = "policy_is_not_null"; - String POLICY = "policy"; + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField RESOURCE_ID = new SearchableModelField<>("resourceId", String.class); + public static final SearchableModelField RESOURCE_SERVER_ID = new SearchableModelField<>("resourceServerId", String.class); + public static final SearchableModelField OWNER = new SearchableModelField<>("owner", String.class); + public static final SearchableModelField REQUESTER = new SearchableModelField<>("requester", String.class); + public static final SearchableModelField SCOPE_ID = new SearchableModelField<>("scopeId", String.class); + public static final SearchableModelField POLICY_ID = new SearchableModelField<>("policyId", String.class); + public static final SearchableModelField GRANTED_TIMESTAMP = new SearchableModelField<>("grantedTimestamp", String.class); + } + + public static enum FilterOption { + ID("id", SearchableFields.ID), + RESOURCE_ID("resource.id", SearchableFields.RESOURCE_ID), + RESOURCE_NAME("resource.name", SearchableFields.RESOURCE_ID), + SCOPE_ID("scope.id", SearchableFields.SCOPE_ID), + SCOPE_IS_NULL("scope_is_null", SearchableFields.SCOPE_ID), + OWNER("owner", SearchableFields.OWNER), + GRANTED("granted", SearchableFields.GRANTED_TIMESTAMP), + REQUESTER("requester", SearchableFields.REQUESTER), + REQUESTER_IS_NULL("requester_is_null", SearchableFields.REQUESTER), + POLICY_IS_NOT_NULL("policy_is_not_null", SearchableFields.POLICY_ID), + POLICY_ID("policy.id", SearchableFields.POLICY_ID) + ; + + private final String name; + private final SearchableModelField searchableModelField; + + FilterOption(String name, SearchableModelField searchableModelField) { + this.name = name; + this.searchableModelField = searchableModelField; + } + + + public String getName() { + return name; + } + + public SearchableModelField getSearchableModelField() { + return searchableModelField; + } + } /** * Returns the unique identifier for this instance. 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 d9e1a0a5f4a..8d5d35ba609 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 @@ -20,6 +20,7 @@ package org.keycloak.authorization.model; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.storage.SearchableModelField; import java.util.Map; import java.util.Set; @@ -31,6 +32,49 @@ import java.util.Set; */ public interface Policy { + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField NAME = new SearchableModelField<>("name", String.class); + public static final SearchableModelField RESOURCE_SERVER_ID = new SearchableModelField<>("resourceServerId", String.class); + public static final SearchableModelField RESOURCE_ID = new SearchableModelField<>("resourceId", String.class); + public static final SearchableModelField SCOPE_ID = new SearchableModelField<>("scopeId", String.class); + public static final SearchableModelField TYPE = new SearchableModelField<>("type", String.class); + public static final SearchableModelField OWNER = new SearchableModelField<>("owner", String.class); + public static final SearchableModelField CONFIG = new SearchableModelField<>("config", String.class); + public static final SearchableModelField ASSOCIATED_POLICY_ID = new SearchableModelField<>("associatedPolicyId", String.class); + } + + public static enum FilterOption { + ID("id", SearchableFields.ID), + PERMISSION("permission", SearchableFields.TYPE), + OWNER("owner", SearchableFields.OWNER), + OWNER_IS_NOT_NULL("owner_is_not_null", SearchableFields.OWNER), + RESOURCE_ID("resources.id", SearchableFields.RESOURCE_ID), + SCOPE_ID("scopes.id", SearchableFields.SCOPE_ID), + CONFIG("config", SearchableFields.CONFIG), + TYPE("type", SearchableFields.TYPE), + NAME("name", SearchableFields.NAME); + + private final String name; + private final SearchableModelField searchableModelField; + + FilterOption(String name, SearchableModelField searchableModelField) { + this.name = name; + this.searchableModelField = searchableModelField; + } + + + public String getName() { + return name; + } + + public SearchableModelField getSearchableModelField() { + return searchableModelField; + } + } + + public static final String CONFIG_SEPARATOR = "##"; + /** * Returns the unique identifier for this instance. * @@ -163,6 +207,4 @@ public interface Policy { void addResource(Resource resource); void removeResource(Resource resource); - - boolean isFetched(String association); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java index 742f7a296a3..349a2e1d6c6 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java @@ -18,6 +18,8 @@ package org.keycloak.authorization.model; +import org.keycloak.storage.SearchableModelField; + import java.util.List; import java.util.Map; import java.util.Set; @@ -29,7 +31,47 @@ import java.util.Set; */ public interface Resource { - String EXACT_NAME = "EXACT_NAME"; + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField NAME = new SearchableModelField<>("name", String.class); + public static final SearchableModelField RESOURCE_SERVER_ID = new SearchableModelField<>("resourceServerId", String.class); + public static final SearchableModelField OWNER = new SearchableModelField<>("owner", String.class); + public static final SearchableModelField TYPE = new SearchableModelField<>("type", String.class); + + public static final SearchableModelField URI = new SearchableModelField<>("uris", String.class); + public static final SearchableModelField SCOPE_ID = new SearchableModelField<>("scope", String.class); + public static final SearchableModelField OWNER_MANAGED_ACCESS = new SearchableModelField<>("ownerManagedAccess", Boolean.class); + } + + public static enum FilterOption { + ID("id", SearchableFields.ID), + NAME("name", SearchableFields.NAME), + EXACT_NAME("name", SearchableFields.NAME), + OWNER("owner", SearchableFields.OWNER), + TYPE("type", SearchableFields.TYPE), + URI("uri", SearchableFields.URI), + URI_NOT_NULL("uri_not_null", SearchableFields.URI), + OWNER_MANAGED_ACCESS("ownerManagedAccess", SearchableFields.OWNER_MANAGED_ACCESS), + SCOPE_ID("scopes.id", SearchableFields.SCOPE_ID); + + private final String name; + private final SearchableModelField searchableModelField; + + FilterOption(String name, SearchableModelField searchableModelField) { + this.name = name; + this.searchableModelField = searchableModelField; + } + + + public String getName() { + return name; + } + + public SearchableModelField getSearchableModelField() { + return searchableModelField; + } + } + /** * Returns the unique identifier for this instance. @@ -182,6 +224,4 @@ public interface Resource { void setAttribute(String name, List values); void removeAttribute(String name); - - boolean isFetched(String association); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/ResourceServer.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/ResourceServer.java index 1d249d05eb4..6082f996498 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/ResourceServer.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/ResourceServer.java @@ -20,6 +20,7 @@ package org.keycloak.authorization.model; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; +import org.keycloak.storage.SearchableModelField; /** * Represents a resource server, whose resources are managed and protected. A resource server is basically an existing @@ -29,6 +30,10 @@ import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; */ public interface ResourceServer { + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + } + /** * Returns the unique identifier for this instance. * diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java index fd90eca2071..955b348288f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java @@ -18,6 +18,8 @@ package org.keycloak.authorization.model; +import org.keycloak.storage.SearchableModelField; + /** * Represents a scope, which is usually associated with one or more resources in order to define the actions that can be performed * or a specific access context. @@ -26,6 +28,34 @@ package org.keycloak.authorization.model; */ public interface Scope { + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField NAME = new SearchableModelField<>("name", String.class); + public static final SearchableModelField RESOURCE_SERVER_ID = new SearchableModelField<>("resourceServerId", String.class); + } + + public static enum FilterOption { + ID("id", SearchableFields.ID), + NAME("name", SearchableFields.NAME); + + private final String name; + private final SearchableModelField searchableModelField; + + FilterOption(String name, SearchableModelField searchableModelField) { + this.name = name; + this.searchableModelField = searchableModelField; + } + + + public String getName() { + return name; + } + + public SearchableModelField getSearchableModelField() { + return searchableModelField; + } + } + /** * Returns the unique identifier for this instance. * 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 a113f26810f..8131081e63d 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java @@ -16,7 +16,7 @@ */ package org.keycloak.authorization.policy.evaluation; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -110,11 +110,11 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionPermis } if (scopes.isEmpty()) { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.REQUESTER, identity.getId()); - filters.put(PermissionTicket.SCOPE_IS_NULL, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, identity.getId()); + filters.put(PermissionTicket.FilterOption.SCOPE_IS_NULL, Boolean.TRUE.toString()); List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer(), -1, -1); @@ -131,11 +131,11 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionPermis scope = scopeStore.findById(scopeId, resourceServer.getId()); } - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.REQUESTER, identity.getId()); - filters.put(PermissionTicket.SCOPE, scope.getId()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, identity.getId()); + filters.put(PermissionTicket.FilterOption.SCOPE_ID, scope.getId()); List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer(), -1, -1); 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 0c253e05ec8..d2e1ad4bb2c 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 @@ -32,13 +32,14 @@ import org.keycloak.authorization.model.ResourceServer; public interface PermissionTicketStore { /** - * Returns a list of {@link PermissionTicket}, filtered by the given attributes. + * Returns count of {@link PermissionTicket}, filtered by the given attributes. * - * @param attributes permission tickets that do not match the attributes are not included with the count. + * @param attributes permission tickets that do not match the attributes are not included with the count; possible filter options are given by {@link PermissionTicket.FilterOption} * @param resourceServerId the resource server id * @return an integer indicating the amount of permission tickets + * @throws IllegalArgumentException when there is an unknown attribute in the {@code attributes} map */ - long count(Map attributes, String resourceServerId); + long count(Map attributes, String resourceServerId); /** * Creates a new {@link PermissionTicket} instance. @@ -99,7 +100,19 @@ 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}, filtered by the given attributes. + * + * @param attributes a map of keys and values to filter on; possible filter options are given by {@link PermissionTicket.FilterOption} + * @param resourceServerId an id of resource server that resulting tickets should belong to. Ignored if {@code null} + * @param firstResult first result to return; Ignored if negative or zero + * @param maxResult maximum number of results to return; Ignored if negative + * @return a list of filtered and paginated permissions + * + * @throws IllegalArgumentException when there is an unknown attribute in the {@code attributes} map + * + */ + List find(Map attributes, String resourceServerId, int firstResult, int maxResult); /** * Returns a list of {@link PermissionTicket} granted to the given {@code userId}. diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java index fee258b0430..e3bae83bb39 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java @@ -18,6 +18,7 @@ package org.keycloak.authorization.store; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -79,11 +80,13 @@ public interface PolicyStore { /** * Returns a list of {@link Policy} associated with a {@link ResourceServer} with the given resourceServerId. * - * @param attributes a map holding the attributes that will be used as a filter + * @param attributes a map holding the attributes that will be used as a filter; possible filter options are given by {@link Policy.FilterOption} * @param resourceServerId the identifier of a resource server * @return a list of policies that belong to the given resource server + * + * @throws IllegalArgumentException when there is an unknown attribute in the {@code attributes} map */ - List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); + List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); /** * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given resourceId. @@ -92,7 +95,13 @@ public interface PolicyStore { * @param resourceServerId the resource server id * @return a list of policies associated with the given resource */ - List findByResource(String resourceId, String resourceServerId); + default List findByResource(String resourceId, String resourceServerId) { + List result = new LinkedList<>(); + + findByResource(resourceId, resourceServerId, result::add); + + return result; + } void findByResource(String resourceId, String resourceServerId, Consumer consumer); @@ -103,7 +112,13 @@ public interface PolicyStore { * @param resourceServerId the resource server id * @return a list of policies associated with the given resource type */ - List findByResourceType(String resourceType, String resourceServerId); + default List findByResourceType(String resourceType, String resourceServerId) { + List result = new LinkedList<>(); + + findByResourceType(resourceType, resourceServerId, result::add); + + return result; + } /** * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given scopeIds. @@ -118,12 +133,23 @@ public interface PolicyStore { * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given resourceId and scopeIds. * * @param scopeIds the id of the scopes - * @param resourceId the id of the resource + * @param resourceId the id of the resource. Ignored if {@code null}. * @param resourceServerId the resource server id * @return a list of policies associated with the given scopes */ - List findByScopeIds(List scopeIds, String resourceId, String resourceServerId); + default List findByScopeIds(List scopeIds, String resourceId, String resourceServerId) { + List result = new LinkedList<>(); + findByScopeIds(scopeIds, resourceId, resourceServerId, result::add); + + return result; + } + + /** + * Effectively the same method as {@link #findByScopeIds(List, String, String)}, however in the end + * the {@code consumer} is fed with the result. + * + */ void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer); /** @@ -144,5 +170,5 @@ public interface PolicyStore { */ List findDependentPolicies(String id, String resourceServerId); - void findByResourceType(String type, String id, Consumer policyConsumer); + void findByResourceType(String type, String resourceServerId, Consumer policyConsumer); } 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 ceb06b12c95..c6f9bc20403 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 @@ -20,6 +20,8 @@ package org.keycloak.authorization.store; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; +import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -39,12 +41,14 @@ public interface ResourceStore { * @param owner the owner of this resource or null if the resource server is the owner * @return an instance backed by the underlying storage implementation */ - Resource create(String name, ResourceServer resourceServer, String owner); + default Resource create(String name, ResourceServer resourceServer, String owner) { + return create(null, name, resourceServer, owner); + } /** *

Creates a {@link Resource} instance backed by this persistent storage implementation. * - * @param id the id of this resource. It must be unique. + * @param id the id of this resource. It must be unique. Will be randomly generated if null. * @param name the name of this resource. It must be unique. * @param resourceServer the resource server to where the given resource belongs to * @param owner the owner of this resource or null if the resource server is the owner @@ -73,7 +77,13 @@ public interface ResourceStore { * @param ownerId the identifier of the owner * @return a list with all resource instances owned by the given owner */ - List findByOwner(String ownerId, String resourceServerId); + default List findByOwner(String ownerId, String resourceServerId) { + List list = new LinkedList<>(); + + findByOwner(ownerId, resourceServerId, list::add); + + return list; + } void findByOwner(String ownerId, String resourceServerId, Consumer consumer); @@ -98,11 +108,13 @@ public interface ResourceStore { /** * Finds all {@link Resource} instances associated with a given resource server. * - * @param attributes a map holding the attributes that will be used as a filter + * @param attributes a map holding the attributes that will be used as a filter; possible filter options are given by {@link Resource.FilterOption} * @param resourceServerId the identifier of the resource server * @return a list with all resources associated with the given resource server + * + * @throws IllegalArgumentException when there is an unknown attribute in the {@code attributes} map */ - List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); + List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); /** * Finds all {@link Resource} associated with a given scope. @@ -110,7 +122,13 @@ public interface ResourceStore { * @param id one or more scope identifiers * @return a list of resources associated with the given scope(s) */ - List findByScope(List id, String resourceServerId); + default List findByScope(List id, String resourceServerId) { + List result = new ArrayList<>(); + + findByScope(id, resourceServerId, result::add); + + return result; + } void findByScope(List scopes, String resourceServerId, Consumer consumer); @@ -139,7 +157,13 @@ public interface ResourceStore { * @param type the type of the resource * @return a list of resources with the given type */ - List findByType(String type, String resourceServerId); + default List findByType(String type, String resourceServerId) { + List list = new LinkedList<>(); + + findByType(type, resourceServerId, list::add); + + return list; + } /** * Finds all {@link Resource} with the given type. @@ -148,7 +172,13 @@ public interface ResourceStore { * @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); + default List findByType(String type, String owner, String resourceServerId) { + List list = new LinkedList<>(); + + findByType(type, owner, resourceServerId, list::add); + + return list; + } /** * Finds all {@link Resource} with the given type. @@ -171,7 +201,13 @@ public interface ResourceStore { */ void findByType(String type, String owner, String resourceServerId, Consumer consumer); - List findByTypeInstance(String type, String resourceServerId); + default List findByTypeInstance(String type, String resourceServerId) { + List list = new LinkedList<>(); + + findByTypeInstance(type, resourceServerId, list::add); + + return list; + } void findByTypeInstance(String type, String resourceServerId, Consumer consumer); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java index 011b6ab2fee..4b96cbff53d 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java @@ -40,13 +40,15 @@ public interface ScopeStore { * * @return a new instance of {@link Scope} */ - Scope create(String name, ResourceServer resourceServer); + default Scope create(String name, ResourceServer resourceServer) { + return create(null, name, resourceServer); + } /** * Creates a new {@link Scope} instance. The new instance is not necessarily persisted though, which may require * a call to the {#save} method to actually make it persistent. * - * @param id the id of the scope + * @param id the id of the scope. Is generated randomly when null * @param name the name of the scope * @param resourceServer the resource server to which this scope belongs * @@ -92,10 +94,12 @@ public interface ScopeStore { /** * Returns a list of {@link Scope} associated with a {@link ResourceServer} with the given resourceServerId. * - * @param attributes a map holding the attributes that will be used as a filter + * @param attributes a map holding the attributes that will be used as a filter; possible filter options are given by {@link Scope.FilterOption} * @param resourceServerId the identifier of a resource server - * * @return a list of scopes that belong to the given resource server + * + * @throws IllegalArgumentException when there is an unknown attribute in the {@code attributes} map + * */ - List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); -} \ No newline at end of file + List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult); +} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java index f6e055504aa..ce29a3370a0 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java @@ -18,7 +18,7 @@ package org.keycloak.authorization.store.syncronization; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -56,10 +56,10 @@ public class ClientApplicationSynchronizer implements Synchronizer attributes = new HashMap<>(); + Map attributes = new EnumMap<>(Policy.FilterOption.class); - attributes.put("type", new String[] {"client"}); - attributes.put("config:clients", new String[] {event.getClient().getId()}); + attributes.put(Policy.FilterOption.TYPE, new String[] {"client"}); + attributes.put(Policy.FilterOption.CONFIG, new String[] {"clients", event.getClient().getId()}); List search = storeFactory.getPolicyStore().findByResourceServer(attributes, null, -1, -1); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/GroupSynchronizer.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/GroupSynchronizer.java index 247ba82b714..4eabeb93294 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/GroupSynchronizer.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/GroupSynchronizer.java @@ -17,7 +17,7 @@ package org.keycloak.authorization.store.syncronization; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,10 +45,10 @@ public class GroupSynchronizer implements Synchronizer attributes = new HashMap<>(); + Map attributes = new EnumMap<>(Policy.FilterOption.class); - attributes.put("type", new String[] {"group"}); - attributes.put("config:groups", new String[] {group.getId()}); + attributes.put(Policy.FilterOption.TYPE, new String[] {"group"}); + attributes.put(Policy.FilterOption.CONFIG, new String[] {"groups", group.getId()}); List search = policyStore.findByResourceServer(attributes, null, -1, -1); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java index 892c9050dcf..0d65ce1a96f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java @@ -17,7 +17,7 @@ package org.keycloak.authorization.store.syncronization; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -58,10 +58,10 @@ public class UserSynchronizer implements Synchronizer { StoreFactory storeFactory = authorizationProvider.getStoreFactory(); PolicyStore policyStore = storeFactory.getPolicyStore(); UserModel userModel = event.getUser(); - Map attributes = new HashMap<>(); + Map attributes = new EnumMap<>(Policy.FilterOption.class); - attributes.put("type", new String[] {"user"}); - attributes.put("config:users", new String[] {userModel.getId()}); + attributes.put(Policy.FilterOption.TYPE, new String[] {"user"}); + attributes.put(Policy.FilterOption.CONFIG, new String[] {"users", userModel.getId()}); List search = policyStore.findByResourceServer(attributes, null, -1, -1); @@ -112,17 +112,17 @@ public class UserSynchronizer implements Synchronizer { StoreFactory storeFactory = authorizationProvider.getStoreFactory(); PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore(); UserModel userModel = event.getUser(); - Map attributes = new HashMap<>(); + Map attributes = new EnumMap<>(PermissionTicket.FilterOption.class); - attributes.put(PermissionTicket.OWNER, userModel.getId()); + attributes.put(PermissionTicket.FilterOption.OWNER, userModel.getId()); for (PermissionTicket ticket : ticketStore.find(attributes, null, -1, -1)) { ticketStore.delete(ticket.getId()); } - attributes = new HashMap<>(); + attributes.clear(); - attributes.put(PermissionTicket.REQUESTER, userModel.getId()); + attributes.put(PermissionTicket.FilterOption.REQUESTER, userModel.getId()); for (PermissionTicket ticket : ticketStore.find(attributes, null, -1, -1)) { ticketStore.delete(ticket.getId()); diff --git a/server-spi-private/src/main/java/org/keycloak/utils/StreamsUtil.java b/server-spi-private/src/main/java/org/keycloak/utils/StreamsUtil.java index 617c6c11d92..f216b435530 100644 --- a/server-spi-private/src/main/java/org/keycloak/utils/StreamsUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/utils/StreamsUtil.java @@ -17,8 +17,12 @@ package org.keycloak.utils; +import java.util.HashSet; import java.util.Iterator; +import java.util.Set; import java.util.Spliterators; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -72,4 +76,15 @@ public class StreamsUtil { return originalStream; } + + /** + * distinctByKey is not supposed to be used with parallel streams + * + * To make this method synchronized use {@code ConcurrentHashMap} instead of HashSet + * + */ + public static Predicate distinctByKey(Function keyExtractor) { + Set seen = new HashSet<>(); + return t -> seen.add(keyExtractor.apply(t)); + } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java index 89212b679a1..155b9810931 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java @@ -45,17 +45,17 @@ public class PermissionService extends PolicyService { protected PolicyTypeService doCreatePolicyTypeResource(String type) { return new PolicyTypeService(type, resourceServer, authorization, auth, adminEvent) { @Override - protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { - filters.put("permission", new String[] {Boolean.TRUE.toString()}); - filters.put("type", new String[] {type}); + protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { + filters.put(Policy.FilterOption.PERMISSION, new String[] {Boolean.TRUE.toString()}); + filters.put(Policy.FilterOption.TYPE, new String[] {type}); return super.doSearch(firstResult, maxResult, fields, filters); } }; } @Override - protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { - filters.put("permission", new String[] {Boolean.TRUE.toString()}); + protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { + filters.put(Policy.FilterOption.PERMISSION, new String[] {Boolean.TRUE.toString()}); return super.doSearch(firstResult, maxResult, fields, filters); } 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 13a4304a65b..34e106abf9a 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -18,7 +18,7 @@ package org.keycloak.authorization.admin; import java.io.IOException; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -184,22 +184,22 @@ public class PolicyService { this.auth.realm().requireViewAuthorization(); } - Map search = new HashMap<>(); + Map search = new EnumMap<>(Policy.FilterOption.class); if (id != null && !"".equals(id.trim())) { - search.put("id", new String[] {id}); + search.put(Policy.FilterOption.ID, new String[] {id}); } if (name != null && !"".equals(name.trim())) { - search.put("name", new String[] {name}); + search.put(Policy.FilterOption.NAME, new String[] {name}); } if (type != null && !"".equals(type.trim())) { - search.put("type", new String[] {type}); + search.put(Policy.FilterOption.TYPE, new String[] {type}); } if (owner != null && !"".equals(owner.trim())) { - search.put("owner", new String[] {owner}); + search.put(Policy.FilterOption.OWNER, new String[] {owner}); } StoreFactory storeFactory = authorization.getStoreFactory(); @@ -209,12 +209,12 @@ public class PolicyService { Resource resourceModel = resourceStore.findById(resource, resourceServer.getId()); if (resourceModel == null) { - Map resourceFilters = new HashMap<>(); + Map resourceFilters = new EnumMap<>(Resource.FilterOption.class); - resourceFilters.put("name", new String[]{resource}); + resourceFilters.put(Resource.FilterOption.NAME, new String[]{resource}); if (owner != null) { - resourceFilters.put("owner", new String[]{owner}); + resourceFilters.put(Resource.FilterOption.OWNER, new String[]{owner}); } Set resources = resourceStore.findByResourceServer(resourceFilters, resourceServer.getId(), -1, 1).stream().map(Resource::getId).collect(Collectors.toSet()); @@ -223,9 +223,9 @@ public class PolicyService { return Response.noContent().build(); } - search.put("resource", resources.toArray(new String[resources.size()])); + search.put(Policy.FilterOption.RESOURCE_ID, resources.toArray(new String[resources.size()])); } else { - search.put("resource", new String[] {resourceModel.getId()}); + search.put(Policy.FilterOption.RESOURCE_ID, new String[] {resourceModel.getId()}); } } @@ -234,9 +234,9 @@ public class PolicyService { Scope scopeModel = scopeStore.findById(scope, resourceServer.getId()); if (scopeModel == null) { - Map scopeFilters = new HashMap<>(); + Map scopeFilters = new EnumMap<>(Scope.FilterOption.class); - scopeFilters.put("name", new String[]{scope}); + scopeFilters.put(Scope.FilterOption.NAME, new String[]{scope}); Set scopes = scopeStore.findByResourceServer(scopeFilters, resourceServer.getId(), -1, 1).stream().map(Scope::getId).collect(Collectors.toSet()); @@ -244,14 +244,14 @@ public class PolicyService { return Response.noContent().build(); } - search.put("scope", scopes.toArray(new String[scopes.size()])); + search.put(Policy.FilterOption.SCOPE_ID, scopes.toArray(new String[scopes.size()])); } else { - search.put("scope", new String[] {scopeModel.getId()}); + search.put(Policy.FilterOption.SCOPE_ID, new String[] {scopeModel.getId()}); } } if (permission != null) { - search.put("permission", new String[] {permission.toString()}); + search.put(Policy.FilterOption.PERMISSION, new String[] {permission.toString()}); } return Response.ok( @@ -263,7 +263,7 @@ public class PolicyService { return ModelToRepresentation.toRepresentation(model, authorization, true, false, fields != null && fields.equals("*")); } - protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { + protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); return policyStore.findByResourceServer(filters, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream() .map(policy -> toRepresentation(policy, fields, authorization)) diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java index 1d28a9bfcfe..22f5f80ec69 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java @@ -92,8 +92,8 @@ public class PolicyTypeService extends PolicyService { } @Override - protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { - filters.put("type", new String[] {type}); + protected List doSearch(Integer firstResult, Integer maxResult, String fields, Map filters) { + filters.put(Policy.FilterOption.TYPE, new String[] {type}); return super.doSearch(firstResult, maxResult, fields, filters); } } 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 9052c385c49..362df41dc1f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java @@ -23,6 +23,7 @@ import static org.keycloak.models.utils.RepresentationToModel.toModel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -256,10 +257,10 @@ public class ResourceSetService { if (model.getType() != null) { policies.addAll(policyStore.findByResourceType(model.getType(), resourceServer.getId())); - HashMap resourceFilter = new HashMap<>(); + Map resourceFilter = new EnumMap<>(Resource.FilterOption.class); - resourceFilter.put("owner", new String[]{resourceServer.getId()}); - resourceFilter.put("type", new String[]{model.getType()}); + resourceFilter.put(Resource.FilterOption.OWNER, new String[]{resourceServer.getId()}); + resourceFilter.put(Resource.FilterOption.TYPE, new String[]{model.getType()}); for (Resource resourceType : resourceStore.findByResourceServer(resourceFilter, resourceServer.getId(), -1, -1)) { policies.addAll(policyStore.findByResource(resourceType.getId(), resourceServer.getId())); @@ -362,22 +363,18 @@ public class ResourceSetService { deep = true; } - Map search = new HashMap<>(); + Map search = new EnumMap<>(Resource.FilterOption.class); if (id != null && !"".equals(id.trim())) { - search.put("id", new String[] {id}); + search.put(Resource.FilterOption.ID, new String[] {id}); } if (name != null && !"".equals(name.trim())) { - search.put("name", new String[] {name}); - - if (exactName != null && exactName) { - search.put(Resource.EXACT_NAME, new String[] {Boolean.TRUE.toString()}); - } + search.put(exactName != null && exactName ? Resource.FilterOption.EXACT_NAME : Resource.FilterOption.NAME, new String[] {name}); } if (uri != null && !"".equals(uri.trim())) { - search.put("uri", new String[] {uri}); + search.put(Resource.FilterOption.URI, new String[] {uri}); } if (owner != null && !"".equals(owner.trim())) { @@ -394,17 +391,17 @@ public class ResourceSetService { } } - search.put("owner", new String[] {owner}); + search.put(Resource.FilterOption.OWNER, new String[] {owner}); } if (type != null && !"".equals(type.trim())) { - search.put("type", new String[] {type}); + search.put(Resource.FilterOption.TYPE, new String[] {type}); } if (scope != null && !"".equals(scope.trim())) { - HashMap scopeFilter = new HashMap<>(); + Map scopeFilter = new EnumMap<>(Scope.FilterOption.class); - scopeFilter.put("name", new String[] {scope}); + scopeFilter.put(Scope.FilterOption.NAME, new String[] {scope}); List scopes = authorization.getStoreFactory().getScopeStore().findByResourceServer(scopeFilter, resourceServer.getId(), -1, -1); @@ -412,16 +409,16 @@ public class ResourceSetService { return Response.ok(Collections.emptyList()).build(); } - search.put("scope", scopes.stream().map(Scope::getId).toArray(String[]::new)); + search.put(Resource.FilterOption.SCOPE_ID, scopes.stream().map(Scope::getId).toArray(String[]::new)); } List resources = storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS); if (matchingUri != null && matchingUri && resources.isEmpty()) { - HashMap attributes = new HashMap<>(); + Map attributes = new EnumMap<>(Resource.FilterOption.class); - attributes.put("uri_not_null", new String[] {"true"}); - attributes.put("owner", new String[] {resourceServer.getId()}); + attributes.put(Resource.FilterOption.URI_NOT_NULL, new String[] {"true"}); + attributes.put(Resource.FilterOption.OWNER, new String[] {resourceServer.getId()}); List serverResources = storeFactory.getResourceStore().findByResourceServer(attributes, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : -1); diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java index 4148a59a13e..859166b48a7 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java @@ -50,7 +50,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import java.util.Arrays; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -242,14 +242,14 @@ public class ScopeService { @QueryParam("max") Integer maxResult) { this.auth.realm().requireViewAuthorization(); - Map search = new HashMap<>(); + Map search = new EnumMap<>(Scope.FilterOption.class); if (id != null && !"".equals(id.trim())) { - search.put("id", new String[] {id}); + search.put(Scope.FilterOption.ID, new String[] {id}); } if (name != null && !"".equals(name.trim())) { - search.put("name", new String[] {name}); + search.put(Scope.FilterOption.NAME, new String[] {name}); } return Response.ok( diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java index bba1ace1b42..b43fcd7d816 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -189,9 +190,9 @@ public class PolicyEvaluationResponseBuilder { representation.setDescription(policy.getDescription()); if ("uma".equals(representation.getType())) { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.POLICY, policy.getId()); + filters.put(PermissionTicket.FilterOption.POLICY_ID, policy.getId()); List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, policy.getResourceServer().getId(), -1, 1); 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 3a8069c1a31..ebe4a8044c4 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 @@ -47,7 +47,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Map; import java.util.stream.Collectors; @@ -116,10 +116,10 @@ public class PermissionTicketService { if (!match) throw new ErrorResponseException("invalid_resource_id", "Resource set with id [" + representation.getResource() + "] does not have Scope [" + scope.getName() + "]", Response.Status.BAD_REQUEST); - Map attributes = new HashMap<>(); - attributes.put(PermissionTicket.RESOURCE, resource.getId()); - attributes.put(PermissionTicket.SCOPE, scope.getId()); - attributes.put(PermissionTicket.REQUESTER, user.getId()); + Map attributes = new EnumMap<>(PermissionTicket.FilterOption.class); + attributes.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); + attributes.put(PermissionTicket.FilterOption.SCOPE_ID, scope.getId()); + attributes.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); if (!ticketStore.find(attributes, resourceServer.getId(), -1, -1).isEmpty()) throw new ErrorResponseException("invalid_permission", "Permission already exists", Response.Status.BAD_REQUEST); @@ -190,7 +190,7 @@ public class PermissionTicketService { StoreFactory storeFactory = authorization.getStoreFactory(); PermissionTicketStore permissionTicketStore = storeFactory.getPermissionTicketStore(); - Map filters = getFilters(storeFactory, resourceId, scopeId, owner, requester, granted); + Map filters = getFilters(storeFactory, resourceId, scopeId, owner, requester, granted); return Response.ok().entity(permissionTicketStore.find(filters, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS) .stream() @@ -210,22 +210,22 @@ public class PermissionTicketService { @QueryParam("returnNames") Boolean returnNames) { StoreFactory storeFactory = authorization.getStoreFactory(); PermissionTicketStore permissionTicketStore = storeFactory.getPermissionTicketStore(); - Map filters = getFilters(storeFactory, resourceId, scopeId, owner, requester, granted); + Map filters = getFilters(storeFactory, resourceId, scopeId, owner, requester, granted); long count = permissionTicketStore.count(filters, resourceServer.getId()); return Response.ok().entity(count).build(); } - private Map getFilters(StoreFactory storeFactory, + private Map getFilters(StoreFactory storeFactory, String resourceId, String scopeId, String owner, String requester, Boolean granted) { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); if (resourceId != null) { - filters.put(PermissionTicket.RESOURCE, resourceId); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resourceId); } if (scopeId != null) { @@ -236,19 +236,19 @@ public class PermissionTicketService { scope = scopeStore.findByName(scopeId, resourceServer.getId()); } - filters.put(PermissionTicket.SCOPE, scope != null ? scope.getId() : scopeId); + filters.put(PermissionTicket.FilterOption.SCOPE_ID, scope != null ? scope.getId() : scopeId); } if (owner != null) { - filters.put(PermissionTicket.OWNER, getUserId(owner)); + filters.put(PermissionTicket.FilterOption.OWNER, getUserId(owner)); } if (requester != null) { - filters.put(PermissionTicket.REQUESTER, getUserId(requester)); + filters.put(PermissionTicket.FilterOption.REQUESTER, getUserId(requester)); } if (granted != null) { - filters.put(PermissionTicket.GRANTED, granted.toString()); + filters.put(PermissionTicket.FilterOption.GRANTED, granted.toString()); } return filters; diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java index c0aba8acf20..f5041cd42c3 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -77,10 +78,10 @@ public class AuthorizationBean { public Collection getResourcesWaitingOthersApproval() { if (resourcesWaitingOthersApproval == null) { - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.REQUESTER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters)); } @@ -90,10 +91,10 @@ public class AuthorizationBean { public Collection getResourcesWaitingApproval() { if (requestsWaitingPermission == null) { - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.OWNER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.OWNER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); requestsWaitingPermission = toResourceRepresentation(findPermissions(filters)); } @@ -113,10 +114,10 @@ public class AuthorizationBean { public Collection getSharedResources() { if (userSharedResources == null) { - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.REQUESTER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); @@ -281,10 +282,10 @@ public class AuthorizationBean { public Collection getShares() { if (shares == null) { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, this.resource.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, this.resource.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); shares = toPermissionRepresentation(findPermissions(filters)); } @@ -293,14 +294,14 @@ public class AuthorizationBean { } public Collection getPolicies() { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(Policy.FilterOption.class); - filters.put("type", new String[] {"uma"}); - filters.put("resource", new String[] {this.resource.getId()}); + filters.put(Policy.FilterOption.TYPE, new String[] {"uma"}); + filters.put(Policy.FilterOption.RESOURCE_ID, new String[] {this.resource.getId()}); if (getUserOwner() != null) { - filters.put("owner", new String[] {getUserOwner().getId()}); + filters.put(Policy.FilterOption.OWNER, new String[] {getUserOwner().getId()}); } else { - filters.put("owner", new String[] {getClientOwner().getId()}); + filters.put(Policy.FilterOption.OWNER, new String[] {getClientOwner().getId()}); } List policies = authorization.getStoreFactory().getPolicyStore().findByResourceServer(filters, getResourceServer().getId(), -1, -1); @@ -311,9 +312,9 @@ public class AuthorizationBean { return policies.stream() .filter(policy -> { - Map filters1 = new HashMap<>(); + Map filters1 = new EnumMap<>(PermissionTicket.FilterOption.class); - filters1.put(PermissionTicket.POLICY, policy.getId()); + filters1.put(PermissionTicket.FilterOption.POLICY_ID, policy.getId()); return authorization.getStoreFactory().getPermissionTicketStore().find(filters1, resourceServer.getId(), -1, 1) .isEmpty(); @@ -366,7 +367,7 @@ public class AuthorizationBean { return requests.values(); } - private List findPermissions(Map filters) { + private List findPermissions(Map filters) { return authorization.getStoreFactory().getPermissionTicketStore().find(filters, null, -1, -1); } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index 1acc644814a..d6211d90c8b 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -102,7 +102,7 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.EnumMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -837,15 +837,15 @@ public class AccountFormService extends AbstractSecuredLocalService { policyStore.delete(policy.getId()); } } else { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.REQUESTER, session.users().getUserByUsername(realm, requester).getId()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, session.users().getUserByUsername(realm, requester).getId()); if (isRevoke) { - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); } else { - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); } List tickets = ticketStore.find(filters, resource.getResourceServer(), -1, -1); @@ -931,11 +931,11 @@ public class AccountFormService extends AbstractSecuredLocalService { return account.setError(Status.BAD_REQUEST, Messages.INVALID_USER).createResponse(AccountPages.RESOURCE_DETAIL); } - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.OWNER, auth.getUser().getId()); - filters.put(PermissionTicket.REQUESTER, user.getId()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); + filters.put(PermissionTicket.FilterOption.OWNER, auth.getUser().getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); List tickets = ticketStore.find(filters, resource.getResourceServer(), -1, -1); @@ -1003,15 +1003,15 @@ public class AccountFormService extends AbstractSecuredLocalService { return ErrorResponse.error("Invalid resource", Response.Status.BAD_REQUEST); } - HashMap filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.REQUESTER, auth.getUser().getId()); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, auth.getUser().getId()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); if ("cancel".equals(action)) { - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); } else if ("cancelRequest".equals(action)) { - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); } for (PermissionTicket ticket : ticketStore.find(filters, resource.getResourceServer(), -1, -1)) { diff --git a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java index d9d70fb18a6..6e7438edbbe 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java @@ -28,6 +28,7 @@ import javax.ws.rs.core.Response; import java.util.Calendar; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -80,11 +81,11 @@ public class ResourceService extends AbstractResourceService { @Path("permissions") @Produces(MediaType.APPLICATION_JSON) public Collection toPermissions() { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.OWNER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.FilterOption.OWNER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); Collection resources = toPermissions(ticketStore.find(filters, null, -1, -1)); Collection permissions = Collections.EMPTY_LIST; @@ -125,14 +126,14 @@ public class ResourceService extends AbstractResourceService { throw new BadRequestException("invalid_permissions"); } - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); for (Permission permission : permissions) { UserModel user = getUser(permission.getUsername()); - filters.put(PermissionTicket.REQUESTER, user.getId()); + filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); List tickets = ticketStore.find(filters, resource.getResourceServer(), -1, -1); @@ -187,11 +188,11 @@ public class ResourceService extends AbstractResourceService { @Path("permissions/requests") @Produces(MediaType.APPLICATION_JSON) public Collection getPermissionRequests() { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.OWNER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.FilterOption.OWNER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); Map requests = new HashMap<>(); diff --git a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java index 0b040e58cf1..a602fc24296 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java @@ -27,7 +27,7 @@ import javax.ws.rs.core.Link; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -64,12 +64,13 @@ public class ResourcesService extends AbstractResourceService { public Response getResources(@QueryParam("name") String name, @QueryParam("first") Integer first, @QueryParam("max") Integer max) { - Map filters = new HashMap<>(); + Map filters = + new EnumMap<>(org.keycloak.authorization.model.Resource.FilterOption.class); - filters.put("owner", new String[] { user.getId() }); + filters.put(org.keycloak.authorization.model.Resource.FilterOption.OWNER, new String[] { user.getId() }); if (name != null) { - filters.put("name", new String[] { name }); + filters.put(org.keycloak.authorization.model.Resource.FilterOption.NAME, new String[] { name }); } return queryResponse((f, m) -> resourceStore.findByResourceServer(filters, null, f, m).stream() @@ -117,10 +118,10 @@ public class ResourcesService extends AbstractResourceService { @Path("pending-requests") @Produces(MediaType.APPLICATION_JSON) public Response getPendingRequests() { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.REQUESTER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString()); final List permissionTickets = ticketStore.find(filters, null, -1, -1); @@ -160,11 +161,11 @@ public class ResourcesService extends AbstractResourceService { List tickets; if (withRequesters) { - Map filters = new HashMap<>(); + Map filters = new EnumMap<>(PermissionTicket.FilterOption.class); - filters.put(PermissionTicket.OWNER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.FilterOption.OWNER, user.getId()); + filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId()); tickets = ticketStore.find(filters, null, -1, -1); } else { diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index b4b38418b36..7d31d253950 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -48,16 +48,14 @@ import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; -import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Stream; import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction; +import static org.keycloak.utils.StreamsUtil.distinctByKey; import static org.keycloak.utils.StreamsUtil.paginatedStream; /** @@ -175,17 +173,6 @@ public class UserStorageManager extends AbstractStorageManager} instead of HashSet - * - */ - private static Predicate distinctByKey(Function keyExtractor) { - Set seen = new HashSet<>(); - return t -> seen.add(keyExtractor.apply(t)); - } - // removeDuplicates method may cause concurrent issues, it should not be used on parallel streams private static Stream removeDuplicates(Stream withDuplicates) { return withDuplicates.filter(distinctByKey(UserModel::getId)); diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index 38c456f542b..149c7ae81b1 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -58,7 +58,7 @@ }, "authorizationPersister": { - "provider": "${keycloak.authorization.provider:}" + "provider": "${keycloak.authorization.provider:jpa}" }, "theme": {