diff --git a/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java index 82fede21cc..f0b3f08633 100755 --- a/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java +++ b/common/src/main/java/org/keycloak/common/util/MultivaluedHashMap.java @@ -33,6 +33,14 @@ public class MultivaluedHashMap extends HashMap> public MultivaluedHashMap() { } + public MultivaluedHashMap(Map> map) { + if (map == null) { + throw new IllegalArgumentException("Map can not be null"); + } + putAll(map); + } + + public MultivaluedHashMap(MultivaluedHashMap config) { addAll(config); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultLazyLoader.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultLazyLoader.java new file mode 100644 index 0000000000..44b97b0caf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultLazyLoader.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.cache.infinispan; + +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Default implementation of {@link DefaultLazyLoader} that only fetches data once. This implementation is not thread-safe + * and cached data is assumed to not be shared across different threads to sync state. + * + * @author Pedro Igor + */ +public class DefaultLazyLoader implements LazyLoader { + + private final Function loader; + private Supplier fallback; + private D data; + + public DefaultLazyLoader(Function loader, Supplier fallback) { + this.loader = loader; + this.fallback = fallback; + } + + @Override + public D get(Supplier sourceSupplier) { + if (data == null) { + S source = sourceSupplier.get(); + data = source == null ? fallback.get() : this.loader.apply(source); + } + return data; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java index 0e69c68e10..6a35951585 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java @@ -29,29 +29,33 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; /** * @author Bill Burke * @version $Revision: 1 $ */ public class GroupAdapter implements GroupModel { + + protected final CachedGroup cached; + protected final RealmCacheSession cacheSession; + protected final KeycloakSession keycloakSession; + protected final RealmModel realm; + private final Supplier modelSupplier; protected volatile GroupModel updated; - protected CachedGroup cached; - protected RealmCacheSession cacheSession; - protected KeycloakSession keycloakSession; - protected RealmModel realm; public GroupAdapter(CachedGroup cached, RealmCacheSession cacheSession, KeycloakSession keycloakSession, RealmModel realm) { this.cached = cached; this.cacheSession = cacheSession; this.keycloakSession = keycloakSession; this.realm = realm; + modelSupplier = this::getGroupModel; } protected void getDelegateForUpdate() { if (updated == null) { cacheSession.registerGroupInvalidation(cached.getId()); - updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm); + updated = modelSupplier.get(); if (updated == null) throw new IllegalStateException("Not found in database"); } } @@ -128,19 +132,19 @@ public class GroupAdapter implements GroupModel { @Override public String getFirstAttribute(String name) { if (isUpdated()) return updated.getFirstAttribute(name); - return cached.getAttributes().getFirst(name); + return cached.getAttributes(modelSupplier).getFirst(name); } @Override public List getAttribute(String name) { - List values = cached.getAttributes().get(name); + List values = cached.getAttributes(modelSupplier).get(name); if (values == null) return null; return values; } @Override public Map> getAttributes() { - return cached.getAttributes(); + return cached.getAttributes(modelSupplier); } @Override @@ -178,7 +182,7 @@ public class GroupAdapter implements GroupModel { @Override public boolean hasRole(RoleModel role) { if (isUpdated()) return updated.hasRole(role); - if (cached.getRoleMappings().contains(role.getId())) return true; + if (cached.getRoleMappings(modelSupplier).contains(role.getId())) return true; Set mappings = getRoleMappings(); for (RoleModel mapping: mappings) { @@ -197,7 +201,7 @@ public class GroupAdapter implements GroupModel { public Set getRoleMappings() { if (isUpdated()) return updated.getRoleMappings(); Set roles = new HashSet(); - for (String id : cached.getRoleMappings()) { + for (String id : cached.getRoleMappings(modelSupplier)) { RoleModel roleById = keycloakSession.realms().getRoleById(id, realm); if (roleById == null) { // chance that role was removed, so just delegate to persistence and get user invalidated @@ -233,7 +237,7 @@ public class GroupAdapter implements GroupModel { public Set getSubGroups() { if (isUpdated()) return updated.getSubGroups(); Set subGroups = new HashSet<>(); - for (String id : cached.getSubGroups()) { + for (String id : cached.getSubGroups(modelSupplier)) { GroupModel subGroup = keycloakSession.realms().getGroupById(id, realm); if (subGroup == null) { // chance that role was removed, so just delegate to persistence and get user invalidated @@ -267,4 +271,8 @@ public class GroupAdapter implements GroupModel { getDelegateForUpdate(); updated.removeChild(subGroup); } + + private GroupModel getGroupModel() { + return cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm); + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/LazyLoader.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/LazyLoader.java new file mode 100644 index 0000000000..b37cde7d07 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/LazyLoader.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.cache.infinispan; + +import java.util.function.Supplier; + +/** + *

A functional interface that can be used to return data {@code D} from a source {@code S} where implementations are free to define how and when + * data is fetched from source as well how it is internally cached. + * + *

The source does not need to worry about caching data but always fetch data as demanded. The way data will actually be cached is an implementation detail. + * + * @author Pedro Igor + * @see DefaultLazyLoader + */ +public interface LazyLoader { + + /** + * Returns data from the given {@code source}. Data is only fetched from {@code source} once and only if necessary, it is + * up to implementations to decide the momentum to actually fetch data from source. + * + * @param source the source from where data will be fetched. + * @return the data from source + */ + D get(Supplier source); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 0056edf493..63cfb65d3d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -35,30 +35,34 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; /** * @author Bill Burke * @version $Revision: 1 $ */ public class UserAdapter implements CachedUserModel { + + private final Supplier modelSupplier; + protected final CachedUser cached; + protected final UserCacheSession userProviderCache; + protected final KeycloakSession keycloakSession; + protected final RealmModel realm; protected volatile UserModel updated; - protected CachedUser cached; - protected UserCacheSession userProviderCache; - protected KeycloakSession keycloakSession; - protected RealmModel realm; public UserAdapter(CachedUser cached, UserCacheSession userProvider, KeycloakSession keycloakSession, RealmModel realm) { this.cached = cached; this.userProviderCache = userProvider; this.keycloakSession = keycloakSession; this.realm = realm; + this.modelSupplier = this::getUserModel; } @Override public UserModel getDelegateForUpdate() { if (updated == null) { userProviderCache.registerUserInvalidation(realm, cached); - updated = userProviderCache.getDelegate().getUserById(getId(), realm); + updated = modelSupplier.get(); if (updated == null) throw new IllegalStateException("Not found in database"); } return updated; @@ -147,26 +151,26 @@ public class UserAdapter implements CachedUserModel { @Override public String getFirstAttribute(String name) { if (updated != null) return updated.getFirstAttribute(name); - return cached.getAttributes().getFirst(name); + return cached.getAttributes(modelSupplier).getFirst(name); } @Override public List getAttribute(String name) { if (updated != null) return updated.getAttribute(name); - List result = cached.getAttributes().get(name); + List result = cached.getAttributes(modelSupplier).get(name); return (result == null) ? Collections.emptyList() : result; } @Override public Map> getAttributes() { if (updated != null) return updated.getAttributes(); - return cached.getAttributes(); + return cached.getAttributes(modelSupplier); } @Override public Set getRequiredActions() { if (updated != null) return updated.getRequiredActions(); - return cached.getRequiredActions(); + return cached.getRequiredActions(modelSupplier); } @Override @@ -301,7 +305,7 @@ public class UserAdapter implements CachedUserModel { @Override public boolean hasRole(RoleModel role) { if (updated != null) return updated.hasRole(role); - if (cached.getRoleMappings().contains(role.getId())) return true; + if (cached.getRoleMappings(modelSupplier).contains(role.getId())) return true; Set mappings = getRoleMappings(); for (RoleModel mapping: mappings) { @@ -320,7 +324,7 @@ public class UserAdapter implements CachedUserModel { public Set getRoleMappings() { if (updated != null) return updated.getRoleMappings(); Set roles = new HashSet(); - for (String id : cached.getRoleMappings()) { + for (String id : cached.getRoleMappings(modelSupplier)) { RoleModel roleById = keycloakSession.realms().getRoleById(id, realm); if (roleById == null) { // chance that role was removed, so just delete to persistence and get user invalidated @@ -343,7 +347,7 @@ public class UserAdapter implements CachedUserModel { public Set getGroups() { if (updated != null) return updated.getGroups(); Set groups = new HashSet(); - for (String id : cached.getGroups()) { + for (String id : cached.getGroups(modelSupplier)) { GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm); if (groupModel == null) { // chance that role was removed, so just delete to persistence and get user invalidated @@ -372,7 +376,7 @@ public class UserAdapter implements CachedUserModel { @Override public boolean isMemberOf(GroupModel group) { if (updated != null) return updated.isMemberOf(group); - if (cached.getGroups().contains(group.getId())) return true; + if (cached.getGroups(modelSupplier).contains(group.getId())) return true; Set roles = getGroups(); return RoleUtils.isMember(roles, group); } @@ -391,7 +395,7 @@ public class UserAdapter implements CachedUserModel { return getId().hashCode(); } - - - + private UserModel getUserModel() { + return userProviderCache.getDelegate().getUserById(cached.getId(), realm); + } } 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 4b34cf9229..34785cc6cc 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 @@ -21,6 +21,9 @@ 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.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.Logic; @@ -30,27 +33,32 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; /** * @author Bill Burke * @version $Revision: 1 $ */ public class PolicyAdapter implements Policy, CachedModel { - protected CachedPolicy cached; - protected StoreFactoryCacheSession cacheSession; + + private final Supplier modelSupplier; + protected final CachedPolicy cached; + protected final StoreFactoryCacheSession cacheSession; protected Policy updated; public PolicyAdapter(CachedPolicy cached, StoreFactoryCacheSession cacheSession) { this.cached = cached; this.cacheSession = cacheSession; + this.modelSupplier = this::getPolicyModel; } @Override public Policy getDelegateForUpdate() { if (updated == null) { - updated = cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId()); + updated = modelSupplier.get(); String defaultResourceType = updated.getConfig().get("defaultResourceType"); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), defaultResourceType, cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), defaultResourceType, cached.getResourceServerId()); if (updated == null) throw new IllegalStateException("Not found in database"); } return updated; @@ -98,7 +106,7 @@ public class PolicyAdapter implements Policy, CachedModel { @Override public void setName(String name) { getDelegateForUpdate(); - cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.setName(name); } @@ -142,15 +150,15 @@ public class PolicyAdapter implements Policy, CachedModel { @Override public Map getConfig() { if (isUpdated()) return updated.getConfig(); - return cached.getConfig(); + return cached.getConfig(modelSupplier); } @Override public void setConfig(Map config) { getDelegateForUpdate(); - if (config.containsKey("defaultResourceType") || cached.getConfig().containsKey("defaultResourceType")) { - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), config.get("defaultResourceType"), cached.getResourceServerId()); + if (config.containsKey("defaultResourceType") || cached.getConfig(modelSupplier).containsKey("defaultResourceType")) { + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), config.get("defaultResourceType"), cached.getResourceServerId()); } updated.setConfig(config); @@ -160,7 +168,7 @@ public class PolicyAdapter implements Policy, CachedModel { public void removeConfig(String name) { getDelegateForUpdate(); if (name.equals("defaultResourceType")) { - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); } updated.removeConfig(name); @@ -170,8 +178,8 @@ public class PolicyAdapter implements Policy, CachedModel { public void putConfig(String name, String value) { getDelegateForUpdate(); if (name.equals("defaultResourceType")) { - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), value, cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), value, cached.getResourceServerId()); } updated.putConfig(name, value); } @@ -192,40 +200,49 @@ public class PolicyAdapter implements Policy, CachedModel { @Override public Set getAssociatedPolicies() { - if (isUpdated()) return updated.getAssociatedPolicies(); + if (isUpdated()) { + return updated.getAssociatedPolicies().stream().map(policy -> new PolicyAdapter(cacheSession.createCachedPolicy(policy, policy.getId()), cacheSession)).collect(Collectors.toSet()); + } if (associatedPolicies != null) return associatedPolicies; associatedPolicies = new HashSet<>(); - for (String scopeId : cached.getAssociatedPoliciesIds()) { - associatedPolicies.add(cacheSession.getPolicyStore().findById(scopeId, cached.getResourceServerId())); + PolicyStore policyStore = cacheSession.getPolicyStore(); + String resourceServerId = cached.getResourceServerId(); + for (String id : cached.getAssociatedPoliciesIds(modelSupplier)) { + Policy policy = policyStore.findById(id, resourceServerId); + cacheSession.cachePolicy(policy); + associatedPolicies.add(policy); } - associatedPolicies = Collections.unmodifiableSet(associatedPolicies); - return associatedPolicies; + return associatedPolicies = Collections.unmodifiableSet(associatedPolicies); } protected Set resources; + @Override public Set getResources() { if (isUpdated()) return updated.getResources(); if (resources != null) return resources; resources = new HashSet<>(); - for (String resourceId : cached.getResourcesIds()) { - resources.add(cacheSession.getResourceStore().findById(resourceId, cached.getResourceServerId())); + ResourceStore resourceStore = cacheSession.getResourceStore(); + for (String resourceId : cached.getResourcesIds(modelSupplier)) { + String resourceServerId = cached.getResourceServerId(); + Resource resource = resourceStore.findById(resourceId, resourceServerId); + cacheSession.cacheResource(resource); + resources.add(resource); } - resources = Collections.unmodifiableSet(resources); - return resources; + return resources = Collections.unmodifiableSet(resources); } @Override public void addScope(Scope scope) { getDelegateForUpdate(); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), new HashSet<>(Arrays.asList(scope.getId())), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.addScope(scope); } @Override public void removeScope(Scope scope) { getDelegateForUpdate(); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), new HashSet<>(Arrays.asList(scope.getId())), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.removeScope(scope); } @@ -248,7 +265,7 @@ public class PolicyAdapter implements Policy, CachedModel { getDelegateForUpdate(); HashSet resources = new HashSet<>(); resources.add(resource.getId()); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.addResource(resource); } @@ -258,11 +275,16 @@ public class PolicyAdapter implements Policy, CachedModel { getDelegateForUpdate(); HashSet resources = new HashSet<>(); resources.add(resource.getId()); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.removeResource(resource); } + @Override + public boolean isFetched(String association) { + return modelSupplier.get().isFetched(association); + } + protected Set scopes; @Override @@ -270,11 +292,14 @@ public class PolicyAdapter implements Policy, CachedModel { if (isUpdated()) return updated.getScopes(); if (scopes != null) return scopes; scopes = new HashSet<>(); - for (String scopeId : cached.getScopesIds()) { - scopes.add(cacheSession.getScopeStore().findById(scopeId, cached.getResourceServerId())); + ScopeStore scopeStore = cacheSession.getScopeStore(); + String resourceServerId = cached.getResourceServerId(); + for (String scopeId : cached.getScopesIds(modelSupplier)) { + Scope scope = scopeStore.findById(scopeId, resourceServerId); + cacheSession.cacheScope(scope); + scopes.add(scope); } - scopes = Collections.unmodifiableSet(scopes); - return scopes; + return scopes = Collections.unmodifiableSet(scopes); } @Override @@ -286,7 +311,7 @@ public class PolicyAdapter implements Policy, CachedModel { @Override public void setOwner(String owner) { getDelegateForUpdate(); - cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getScopesIds(), cached.getConfig().get("defaultResourceType"), cached.getResourceServerId()); + cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(modelSupplier), cached.getScopesIds(modelSupplier), cached.getConfig(modelSupplier).get("defaultResourceType"), cached.getResourceServerId()); updated.setOwner(owner); } @@ -304,7 +329,7 @@ public class PolicyAdapter implements Policy, CachedModel { return getId().hashCode(); } - - - + private Policy getPolicyModel() { + return cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId()); + } } 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 97ab8302a3..a22fa1927b 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 @@ -18,7 +18,6 @@ package org.keycloak.models.cache.infinispan.authorization; import org.keycloak.authorization.model.CachedModel; import org.keycloak.authorization.model.PermissionTicket; -import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; @@ -31,7 +30,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -40,20 +39,22 @@ import java.util.stream.Collectors; */ public class ResourceAdapter implements Resource, CachedModel { - protected CachedResource cached; - protected StoreFactoryCacheSession cacheSession; + private final Supplier modelSupplier; + protected final CachedResource cached; + protected final StoreFactoryCacheSession cacheSession; protected Resource updated; public ResourceAdapter(CachedResource cached, StoreFactoryCacheSession cacheSession) { this.cached = cached; this.cacheSession = cacheSession; + this.modelSupplier = this::getResourceModel; } @Override public Resource getDelegateForUpdate() { if (updated == null) { - cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); - updated = cacheSession.getResourceStoreDelegate().findById(cached.getId(), cached.getResourceServerId()); + updated = modelSupplier.get(); + cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); if (updated == null) throw new IllegalStateException("Not found in database"); } return updated; @@ -101,7 +102,7 @@ public class ResourceAdapter implements Resource, CachedModel { @Override public void setName(String name) { getDelegateForUpdate(); - cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); updated.setName(name); } @@ -114,7 +115,7 @@ public class ResourceAdapter implements Resource, CachedModel { @Override public void setDisplayName(String name) { getDelegateForUpdate(); - cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); updated.setDisplayName(name); } @@ -139,13 +140,13 @@ public class ResourceAdapter implements Resource, CachedModel { @Override public Set getUris() { if (isUpdated()) return updated.getUris(); - return cached.getUris(); + return cached.getUris(modelSupplier); } @Override public void updateUris(Set uris) { getDelegateForUpdate(); - cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), uris, cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), uris, cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); updated.updateUris(uris); } @@ -158,7 +159,7 @@ public class ResourceAdapter implements Resource, CachedModel { @Override public void setType(String type) { getDelegateForUpdate(); - cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), type, cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), type, cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); updated.setType(type); } @@ -170,11 +171,10 @@ public class ResourceAdapter implements Resource, CachedModel { if (isUpdated()) return updated.getScopes(); if (scopes != null) return scopes; scopes = new LinkedList<>(); - for (String scopeId : cached.getScopesIds()) { + for (String scopeId : cached.getScopesIds(modelSupplier)) { scopes.add(cacheSession.getScopeStore().findById(scopeId, cached.getResourceServerId())); } - scopes = Collections.unmodifiableList(scopes); - return scopes; + return scopes = Collections.unmodifiableList(scopes); } @Override @@ -192,7 +192,7 @@ public class ResourceAdapter implements Resource, CachedModel { @Override public void setOwnerManagedAccess(boolean ownerManagedAccess) { getDelegateForUpdate(); - cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner()); updated.setOwnerManagedAccess(ownerManagedAccess); } @@ -219,21 +219,21 @@ public class ResourceAdapter implements Resource, CachedModel { } } - cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner()); + cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(modelSupplier), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner()); updated.updateScopes(scopes); } @Override public Map> getAttributes() { if (updated != null) return updated.getAttributes(); - return cached.getAttributes(); + return cached.getAttributes(modelSupplier); } @Override public String getSingleAttribute(String name) { if (updated != null) return updated.getSingleAttribute(name); - List values = cached.getAttributes().getOrDefault(name, Collections.emptyList()); + List values = cached.getAttributes(modelSupplier).getOrDefault(name, Collections.emptyList()); if (values.isEmpty()) { return null; @@ -246,7 +246,7 @@ public class ResourceAdapter implements Resource, CachedModel { public List getAttribute(String name) { if (updated != null) return updated.getAttribute(name); - List values = cached.getAttributes().getOrDefault(name, Collections.emptyList()); + List values = cached.getAttributes(modelSupplier).getOrDefault(name, Collections.emptyList()); if (values.isEmpty()) { return null; @@ -267,6 +267,11 @@ 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; @@ -281,4 +286,7 @@ public class ResourceAdapter implements Resource, CachedModel { return getId().hashCode(); } + private Resource getResourceModel() { + return cacheSession.getResourceStoreDelegate().findById(cached.getId(), cached.getResourceServerId()); + } } 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 c2b60c74bf..04e6eb7056 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 @@ -407,7 +407,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } } - private boolean modelMightExist(String id) { + boolean modelMightExist(String id) { return invalidations.contains(id) || cache.get(id, NonExistentItem.class) == null; } @@ -712,11 +712,11 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer); } - private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId) { + private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId) { return cacheQuery(cacheKey, queryType, resultSupplier, querySupplier, resourceServerId, null); } - private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId, Consumer consumer) { + private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId, Consumer consumer) { Q query = cache.get(cacheKey, queryType); if (query != null) { logger.tracev("cache hit for key: {0}", cacheKey); @@ -730,7 +730,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { cache.addRevisioned(query, startupRevision); if (consumer != null) { for (R resource : model) { - consumer.accept(resource); + consumer.andThen(r -> cacheResource(resource)).accept(resource); } } return model; @@ -799,9 +799,9 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { logger.tracev("by id cache hit: {0}", cached.getId()); } if (cached == null) { - Long loaded = cache.getCurrentRevision(id); if (! modelMightExist(id)) return null; Policy model = getPolicyStoreDelegate().findById(id, resourceServerId); + Long loaded = cache.getCurrentRevision(id); if (model == null) { setModelDoesNotExists(id, loaded); return null; @@ -922,7 +922,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { return getPolicyStoreDelegate().findDependentPolicies(id, resourceServerId); } - private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId, Consumer consumer) { + private List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, Q> querySupplier, String resourceServerId, Consumer consumer) { Q query = cache.get(cacheKey, queryType); if (query != null) { logger.tracev("cache hit for key: {0}", cacheKey); @@ -936,7 +936,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { cache.addRevisioned(query, startupRevision); if (consumer != null) { for (R policy: model) { - consumer.accept(policy); + consumer.andThen(r -> cachePolicy(policy)).accept(policy); } } return model; @@ -1080,4 +1080,46 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { } } + void cachePolicy(Policy model) { + String id = model.getId(); + if (cache.getCache().containsKey(id)) { + return; + } + if (!modelMightExist(id)) { + return; + } + if (invalidations.contains(id)) return; + cache.addRevisioned(createCachedPolicy(model, id), startupRevision); + } + + CachedPolicy createCachedPolicy(Policy model, String id) { + Long loaded = cache.getCurrentRevision(id); + return new CachedPolicy(loaded, model); + } + + void cacheResource(Resource model) { + String id = model.getId(); + if (cache.getCache().containsKey(id)) { + return; + } + Long loaded = cache.getCurrentRevision(id); + if (!modelMightExist(id)) { + return; + } + if (invalidations.contains(id)) return; + cache.addRevisioned(new CachedResource(loaded, model), startupRevision); + } + + void cacheScope(Scope model) { + String id = model.getId(); + if (cache.getCache().containsKey(id)) { + return; + } + Long loaded = cache.getCurrentRevision(id); + if (!modelMightExist(id)) { + return; + } + if (invalidations.contains(id)) return; + cache.addRevisioned(new CachedScope(loaded, model), startupRevision); + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java index 643c8683ee..062668e563 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPolicy.java @@ -21,13 +21,17 @@ package org.keycloak.models.cache.infinispan.authorization.entities; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Scope; +import org.keycloak.models.cache.infinispan.DefaultLazyLoader; +import org.keycloak.models.cache.infinispan.LazyLoader; import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.Logic; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -35,16 +39,16 @@ import java.util.stream.Collectors; */ public class CachedPolicy extends AbstractRevisioned implements InResourceServer { - private String type; - private DecisionStrategy decisionStrategy; - private Logic logic; - private Map config; - private String name; - private String description; - private String resourceServerId; - private Set associatedPoliciesIds; - private Set resourcesIds; - private Set scopesIds; + private final String type; + private final DecisionStrategy decisionStrategy; + private final Logic logic; + private final String name; + private final String description; + private final String resourceServerId; + private final LazyLoader> associatedPoliciesIds; + private final LazyLoader> resourcesIds; + private final LazyLoader> scopesIds; + private final LazyLoader> config; private final String owner; public CachedPolicy(Long revision, Policy policy) { @@ -52,13 +56,38 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer this.type = policy.getType(); this.decisionStrategy = policy.getDecisionStrategy(); this.logic = policy.getLogic(); - this.config = new HashMap(policy.getConfig()); this.name = policy.getName(); this.description = policy.getDescription(); this.resourceServerId = policy.getResourceServer().getId(); - this.associatedPoliciesIds = policy.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet()); - this.resourcesIds = policy.getResources().stream().map(Resource::getId).collect(Collectors.toSet()); - this.scopesIds = policy.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + + if (policy.isFetched("associatedPolicies")) { + Set data = policy.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet()); + this.associatedPoliciesIds = source -> data; + } else { + this.associatedPoliciesIds = new DefaultLazyLoader<>(source -> source.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet()), Collections::emptySet); + } + + if (policy.isFetched("resources")) { + Set data = policy.getResources().stream().map(Resource::getId).collect(Collectors.toSet()); + this.resourcesIds = source -> data; + } else { + this.resourcesIds = new DefaultLazyLoader<>(source -> source.getResources().stream().map(Resource::getId).collect(Collectors.toSet()), Collections::emptySet); + } + + if (policy.isFetched("scopes")) { + Set data = policy.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + this.scopesIds = source -> data; + } else { + this.scopesIds = new DefaultLazyLoader<>(source -> source.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()), Collections::emptySet); + } + + if (policy.isFetched("config")) { + Map data = new HashMap<>(policy.getConfig()); + this.config = source -> data; + } else { + this.config = new DefaultLazyLoader<>(source -> new HashMap<>(source.getConfig()), Collections::emptyMap); + } + this.owner = policy.getOwner(); } @@ -74,8 +103,8 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer return this.logic; } - public Map getConfig() { - return this.config; + public Map getConfig(Supplier policy) { + return this.config.get(policy); } public String getName() { @@ -86,16 +115,16 @@ public class CachedPolicy extends AbstractRevisioned implements InResourceServer return this.description; } - public Set getAssociatedPoliciesIds() { - return this.associatedPoliciesIds; + public Set getAssociatedPoliciesIds(Supplier policy) { + return this.associatedPoliciesIds.get(policy); } - public Set getResourcesIds() { - return this.resourcesIds; + public Set getResourcesIds(Supplier policy) { + return this.resourcesIds.get(policy); } - public Set getScopesIds() { - return this.scopesIds; + public Set getScopesIds(Supplier policy) { + return this.scopesIds.get(policy); } public String getResourceServerId() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java index c631279894..0205550c5c 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java @@ -21,11 +21,16 @@ package org.keycloak.models.cache.infinispan.authorization.entities; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Scope; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.cache.infinispan.DefaultLazyLoader; +import org.keycloak.models.cache.infinispan.LazyLoader; import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -33,29 +38,47 @@ import java.util.stream.Collectors; */ public class CachedResource extends AbstractRevisioned implements InResourceServer { - private String resourceServerId; - private String iconUri; - private String owner; - private String type; - private String name; - private String displayName; - private Set uris; - private Set scopesIds; - private boolean ownerManagedAccess; - private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); + private final String resourceServerId; + private final String iconUri; + private final String owner; + private final String type; + private final String name; + private final String displayName; + private final boolean ownerManagedAccess; + private LazyLoader> scopesIds; + private LazyLoader> uris; + private LazyLoader> attributes; public CachedResource(Long revision, Resource resource) { super(revision, resource.getId()); this.name = resource.getName(); this.displayName = resource.getDisplayName(); - this.uris = resource.getUris(); this.type = resource.getType(); this.owner = resource.getOwner(); this.iconUri = resource.getIconUri(); this.resourceServerId = resource.getResourceServer().getId(); - this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); ownerManagedAccess = resource.isOwnerManagedAccess(); - this.attributes.putAll(resource.getAttributes()); + + if (resource.isFetched("uris")) { + Set data = new HashSet<>(resource.getUris()); + this.uris = source -> data; + } else { + this.uris = new DefaultLazyLoader<>(source -> new HashSet<>(source.getUris()), Collections::emptySet); + } + + if (resource.isFetched("scopes")) { + Set data = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + this.scopesIds = source -> data; + } else { + this.scopesIds = new DefaultLazyLoader<>(source -> source.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()), Collections::emptySet); + } + + if (resource.isFetched("attributes")) { + MultivaluedHashMap data = new MultivaluedHashMap<>(resource.getAttributes()); + this.attributes = source -> data; + } else { + this.attributes = new DefaultLazyLoader<>(source -> new MultivaluedHashMap<>(source.getAttributes()), MultivaluedHashMap::new); + } } @@ -67,8 +90,8 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ return this.displayName; } - public Set getUris() { - return this.uris; + public Set getUris(Supplier source) { + return this.uris.get(source); } public String getType() { @@ -91,11 +114,11 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ return this.resourceServerId; } - public Set getScopesIds() { - return this.scopesIds; + public Set getScopesIds(Supplier source) { + return this.scopesIds.get(source); } - public Map> getAttributes() { - return attributes; + public Map> getAttributes(Supplier source) { + return attributes.get(source); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java index 01ba0cd57b..bf83d27ab4 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java @@ -21,50 +21,51 @@ import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.infinispan.DefaultLazyLoader; +import org.keycloak.models.cache.infinispan.LazyLoader; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; /** * @author Bill Burke * @version $Revision: 1 $ */ public class CachedGroup extends AbstractRevisioned implements InRealm { - private String realm; - private String name; - private String parentId; - private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); - private Set roleMappings = new HashSet<>(); - private Set subGroups = new HashSet<>(); + + private final String realm; + private final String name; + private final String parentId; + private final LazyLoader> attributes; + private final LazyLoader> roleMappings; + private final LazyLoader> subGroups; public CachedGroup(Long revision, RealmModel realm, GroupModel group) { super(revision, group.getId()); this.realm = realm.getId(); this.name = group.getName(); this.parentId = group.getParentId(); - - this.attributes.putAll(group.getAttributes()); - for (RoleModel role : group.getRoleMappings()) { - roleMappings.add(role.getId()); - } - Set subGroups1 = group.getSubGroups(); - if (subGroups1 != null) { - for (GroupModel subGroup : subGroups1) { - subGroups.add(subGroup.getId()); - } - } + this.attributes = new DefaultLazyLoader<>(source -> new MultivaluedHashMap<>(source.getAttributes()), MultivaluedHashMap::new); + this.roleMappings = new DefaultLazyLoader<>(source -> source.getRoleMappings().stream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet); + this.subGroups = new DefaultLazyLoader<>(source -> source.getSubGroups().stream().map(GroupModel::getId).collect(Collectors.toSet()), Collections::emptySet); } public String getRealm() { return realm; } - public MultivaluedHashMap getAttributes() { - return attributes; + public MultivaluedHashMap getAttributes(Supplier group) { + return attributes.get(group); } - public Set getRoleMappings() { - return roleMappings; + public Set getRoleMappings(Supplier group) { + // it may happen that groups were not loaded before so we don't actually need to invalidate entries in the cache + if (group == null) { + return Collections.emptySet(); + } + return roleMappings.get(group); } public String getName() { @@ -75,7 +76,7 @@ public class CachedGroup extends AbstractRevisioned implements InRealm { return parentId; } - public Set getSubGroups() { - return subGroups; + public Set getSubGroups(Supplier group) { + return subGroups.get(group); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java index 68dfc372cb..3a70b67e89 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java @@ -22,32 +22,35 @@ import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.cache.infinispan.DefaultLazyLoader; +import org.keycloak.models.cache.infinispan.LazyLoader; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; /** * @author Bill Burke * @version $Revision: 1 $ */ public class CachedUser extends AbstractExtendableRevisioned implements InRealm { - private String realm; - private String username; - private Long createdTimestamp; - private String firstName; - private String lastName; - private String email; - private boolean emailVerified; - private boolean enabled; - private String federationLink; - private String serviceAccountClientLink; - private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); - private Set requiredActions = new HashSet<>(); - private Set roleMappings = new HashSet<>(); - private Set groups = new HashSet<>(); - private int notBefore; - + private final String realm; + private final String username; + private final Long createdTimestamp; + private final String firstName; + private final String lastName; + private final String email; + private final boolean emailVerified; + private final boolean enabled; + private final String federationLink; + private final String serviceAccountClientLink; + private final int notBefore; + private final LazyLoader> requiredActions; + private final LazyLoader> attributes; + private final LazyLoader> roleMappings; + private final LazyLoader> groups; public CachedUser(Long revision, RealmModel realm, UserModel user, int notBefore) { super(revision, user.getId()); @@ -56,23 +59,16 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm this.createdTimestamp = user.getCreatedTimestamp(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); - this.attributes.putAll(user.getAttributes()); this.email = user.getEmail(); this.emailVerified = user.isEmailVerified(); this.enabled = user.isEnabled(); this.federationLink = user.getFederationLink(); this.serviceAccountClientLink = user.getServiceAccountClientLink(); - this.requiredActions.addAll(user.getRequiredActions()); - for (RoleModel role : user.getRoleMappings()) { - roleMappings.add(role.getId()); - } - Set groupMappings = user.getGroups(); - if (groupMappings != null) { - for (GroupModel group : groupMappings) { - groups.add(group.getId()); - } - } this.notBefore = notBefore; + this.requiredActions = new DefaultLazyLoader<>(UserModel::getRequiredActions, Collections::emptySet); + this.attributes = new DefaultLazyLoader<>(userModel -> new MultivaluedHashMap<>(userModel.getAttributes()), MultivaluedHashMap::new); + this.roleMappings = new DefaultLazyLoader<>(userModel -> userModel.getRoleMappings().stream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet); + this.groups = new DefaultLazyLoader<>(userModel -> userModel.getGroups().stream().map(GroupModel::getId).collect(Collectors.toSet()), Collections::emptySet); } public String getRealm() { @@ -107,16 +103,16 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm return enabled; } - public MultivaluedHashMap getAttributes() { - return attributes; + public MultivaluedHashMap getAttributes(Supplier userModel) { + return attributes.get(userModel); } - public Set getRequiredActions() { - return requiredActions; + public Set getRequiredActions(Supplier userModel) { + return this.requiredActions.get(userModel); } - public Set getRoleMappings() { - return roleMappings; + public Set getRoleMappings(Supplier userModel) { + return roleMappings.get(userModel); } public String getFederationLink() { @@ -127,8 +123,8 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm return serviceAccountClientLink; } - public Set getGroups() { - return groups; + public Set getGroups(Supplier userModel) { + return groups.get(userModel); } public int getNotBefore() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/HasRolePredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/HasRolePredicate.java index 3b548b98b6..2523dc9b26 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/HasRolePredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/HasRolePredicate.java @@ -44,7 +44,7 @@ public class HasRolePredicate implements Predicate } if (value instanceof CachedGroup) { CachedGroup cachedRole = (CachedGroup)value; - if (cachedRole.getRoleMappings().contains(role)) return true; + if (cachedRole.getRoleMappings(null).contains(role)) return true; } if (value instanceof RoleQuery) { RoleQuery roleQuery = (RoleQuery)value; diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java index e235f586d5..96408a61ae 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java @@ -55,13 +55,13 @@ import org.keycloak.representations.idm.authorization.Logic; @NamedQueries( { @NamedQuery(name="findPolicyIdByServerId", query="select p.id from PolicyEntity p where p.resourceServer.id = :serverId "), - @NamedQuery(name="findPolicyIdByName", query="select p.id from PolicyEntity p where p.resourceServer.id = :serverId and p.name = :name"), - @NamedQuery(name="findPolicyIdByResource", query="select p.id from PolicyEntity p inner join p.resources r where p.resourceServer.id = :serverId and (r.resourceServer.id = :serverId and r.id = :resourceId)"), - @NamedQuery(name="findPolicyIdByScope", query="select pe.id from PolicyEntity pe where pe.resourceServer.id = :serverId and pe.id IN (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds)))"), - @NamedQuery(name="findPolicyIdByResourceScope", query="select pe.id from PolicyEntity pe where pe.resourceServer.id = :serverId and pe.id IN (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds))) and pe.id IN (select p.id from ResourceEntity r inner join r.policies p where r.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and r.id in (:resourceId))))"), - @NamedQuery(name="findPolicyIdByNullResourceScope", query="select pe.id from PolicyEntity pe where pe.resourceServer.id = :serverId and pe.id IN (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds))) and pe.resources is empty"), + @NamedQuery(name="findPolicyIdByName", query="select p from PolicyEntity p left join fetch p.associatedPolicies a where p.resourceServer.id = :serverId and p.name = :name"), + @NamedQuery(name="findPolicyIdByResource", query="select p from PolicyEntity p inner join fetch p.resources r left join fetch p.scopes s inner join fetch p.associatedPolicies a where p.resourceServer.id = :serverId and (r.resourceServer.id = :serverId and r.id = :resourceId)"), + @NamedQuery(name="findPolicyIdByScope", query="select pe from PolicyEntity pe left join fetch pe.resources r inner join fetch pe.scopes s inner join fetch pe.associatedPolicies a where pe.resourceServer.id = :serverId and exists (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds) and p.id = pe.id))"), + @NamedQuery(name="findPolicyIdByResourceScope", query="select pe from PolicyEntity pe inner join fetch pe.resources r inner join fetch pe.scopes s inner join fetch pe.associatedPolicies a where pe.resourceServer.id = :serverId and exists (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds) and p.id = pe.id)) and exists (select p.id from ResourceEntity r inner join r.policies p where r.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.id = pe.id and p.type = 'scope' and r.id in (:resourceId))))"), + @NamedQuery(name="findPolicyIdByNullResourceScope", query="select pe from PolicyEntity pe left join fetch pe.resources r inner join fetch pe.scopes s inner join fetch pe.associatedPolicies a where pe.resourceServer.id = :serverId and exists (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.id = pe.id and p.type = 'scope' and s.id in (:scopeIds))) and pe.resources is empty"), @NamedQuery(name="findPolicyIdByType", query="select p.id from PolicyEntity p where p.resourceServer.id = :serverId and p.type = :type"), - @NamedQuery(name="findPolicyIdByResourceType", query="select p.id from PolicyEntity p inner join p.config c where p.resourceServer.id = :serverId and KEY(c) = 'defaultResourceType' and c like :type"), + @NamedQuery(name="findPolicyIdByResourceType", query="select p from PolicyEntity p inner join p.config c inner join fetch p.associatedPolicies a where p.resourceServer.id = :serverId and KEY(c) = 'defaultResourceType' and c like :type"), @NamedQuery(name="findPolicyIdByDependentPolices", query="select p.id from PolicyEntity p inner join p.associatedPolicies ap where p.resourceServer.id = :serverId and (ap.resourceServer.id = :serverId and ap.id = :policyId)"), @NamedQuery(name="deletePolicyByResourceServer", query="delete from PolicyEntity p where p.resourceServer.id = :serverId") } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java index ab3ba82080..4f921548e4 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java @@ -57,13 +57,13 @@ import org.hibernate.annotations.FetchMode; }) @NamedQueries( { - @NamedQuery(name="findResourceIdByOwner", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :owner"), - @NamedQuery(name="findAnyResourceIdByOwner", query="select r.id from ResourceEntity r where r.owner = :owner"), + @NamedQuery(name="findResourceIdByOwner", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :owner"), + @NamedQuery(name="findAnyResourceIdByOwner", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.owner = :owner"), @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and :uri in elements(r.uris)"), - @NamedQuery(name="findResourceIdByName", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :ownerId and r.name = :name"), - @NamedQuery(name="findResourceIdByType", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :ownerId and r.type = :type"), + @NamedQuery(name="findResourceIdByName", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.name = :name"), + @NamedQuery(name="findResourceIdByType", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.type = :type"), @NamedQuery(name="findResourceIdByServerId", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId "), - @NamedQuery(name="findResourceIdByScope", query="select r.id from ResourceEntity r inner join r.scopes s where r.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id in (:scopeIds))"), + @NamedQuery(name="findResourceIdByScope", query="select r from ResourceEntity r inner join r.scopes s where r.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id in (:scopeIds))"), @NamedQuery(name="deleteResourceByResourceServer", query="delete from ResourceEntity r where r.resourceServer.id = :serverId") } ) @@ -80,7 +80,7 @@ public class ResourceEntity { @Column(name = "DISPLAY_NAME") private String displayName; - @ElementCollection(fetch = FetchType.EAGER) + @ElementCollection(fetch = FetchType.LAZY) @Column(name = "VALUE") @CollectionTable(name = "RESOURCE_URIS", joinColumns = { @JoinColumn(name="RESOURCE_ID") }) private Set uris = new HashSet<>(); @@ -101,7 +101,7 @@ public class ResourceEntity { @JoinColumn(name = "RESOURCE_SERVER_ID") private ResourceServerEntity resourceServer; - @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @OneToMany(fetch = FetchType.LAZY, cascade = {}) @JoinTable(name = "RESOURCE_SCOPE", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID")) private List scopes = new LinkedList<>(); @@ -109,7 +109,7 @@ public class ResourceEntity { @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID")) private List policies = new LinkedList<>(); - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="resource") + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="resource", fetch = FetchType.LAZY) @Fetch(FetchMode.SELECT) @BatchSize(size = 20) private Collection attributes = new ArrayList<>(); 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 73c7c7df7e..0dcccb9eb8 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 @@ -40,6 +40,7 @@ import org.keycloak.authorization.jpa.entities.PolicyEntity; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; @@ -90,23 +91,25 @@ public class JPAPolicyStore implements PolicyStore { return null; } - PolicyEntity entity = entityManager.find(PolicyEntity.class, id); - if (entity == null) return null; + PolicyEntity policyEntity = entityManager.find(PolicyEntity.class, id); - return new PolicyAdapter(entity, entityManager, provider.getStoreFactory()); + if (policyEntity == null) { + return null; + } + + return new PolicyAdapter(policyEntity, entityManager, provider.getStoreFactory()); } @Override public Policy findByName(String name, String resourceServerId) { - TypedQuery query = entityManager.createNamedQuery("findPolicyIdByName", String.class); + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByName", PolicyEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("name", name); try { - String id = query.getSingleResult(); - return provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId); + return new PolicyAdapter(query.getSingleResult(), entityManager, provider.getStoreFactory()); } catch (NoResultException ex) { return null; } @@ -203,17 +206,16 @@ public class JPAPolicyStore implements PolicyStore { @Override public void findByResource(String resourceId, String resourceServerId, Consumer consumer) { - TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResource", String.class); + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResource", PolicyEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("resourceId", resourceId); query.setParameter("serverId", resourceServerId); - PolicyStore policyStore = provider.getStoreFactory().getPolicyStore(); + StoreFactory storeFactory = provider.getStoreFactory(); query.getResultList().stream() - .map(id -> policyStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(entity -> new PolicyAdapter(entity, entityManager, storeFactory)) .forEach(consumer::accept); } @@ -228,17 +230,14 @@ public class JPAPolicyStore implements PolicyStore { @Override public void findByResourceType(String resourceType, String resourceServerId, Consumer consumer) { - TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResourceType", String.class); + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResourceType", PolicyEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", resourceType); query.setParameter("serverId", resourceServerId); - PolicyStore policyStore = provider.getStoreFactory().getPolicyStore(); - query.getResultList().stream() - .map(id -> policyStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(id -> new PolicyAdapter(id, entityManager, provider.getStoreFactory())) .forEach(consumer::accept); } @@ -249,20 +248,19 @@ public class JPAPolicyStore implements PolicyStore { } // Use separate subquery to handle DB2 and MSSSQL - TypedQuery query = entityManager.createNamedQuery("findPolicyIdByScope", String.class); + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByScope", PolicyEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("scopeIds", scopeIds); query.setParameter("serverId", resourceServerId); - List result = query.getResultList(); List list = new LinkedList<>(); - for (String id : result) { - Policy policy = provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId); - if (Objects.nonNull(policy)) { - list.add(policy); - } + StoreFactory storeFactory = provider.getStoreFactory(); + + for (PolicyEntity entity : query.getResultList()) { + list.add(new PolicyAdapter(entity, entityManager, storeFactory)); } + return list; } @@ -278,12 +276,12 @@ public class JPAPolicyStore implements PolicyStore { @Override public void findByScopeIds(List scopeIds, String resourceId, String resourceServerId, Consumer consumer) { // Use separate subquery to handle DB2 and MSSSQL - TypedQuery query; + TypedQuery query; if (resourceId == null) { - query = entityManager.createNamedQuery("findPolicyIdByNullResourceScope", String.class); + query = entityManager.createNamedQuery("findPolicyIdByNullResourceScope", PolicyEntity.class); } else { - query = entityManager.createNamedQuery("findPolicyIdByResourceScope", String.class); + query = entityManager.createNamedQuery("findPolicyIdByResourceScope", PolicyEntity.class); query.setParameter("resourceId", resourceId); } @@ -291,11 +289,10 @@ public class JPAPolicyStore implements PolicyStore { query.setParameter("scopeIds", scopeIds); query.setParameter("serverId", resourceServerId); - PolicyStore policyStore = provider.getStoreFactory().getPolicyStore(); + StoreFactory storeFactory = provider.getStoreFactory(); query.getResultList().stream() - .map(id -> policyStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(id -> new PolicyAdapter(id, entityManager, storeFactory)) .forEach(consumer::accept); } 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 034838e314..7a95f7dd97 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 @@ -22,6 +22,7 @@ import org.keycloak.authorization.jpa.entities.ResourceEntity; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; @@ -116,7 +117,7 @@ public class JPAResourceStore implements ResourceStore { queryName = "findAnyResourceIdByOwner"; } - TypedQuery query = entityManager.createNamedQuery(queryName, String.class); + TypedQuery query = entityManager.createNamedQuery(queryName, ResourceEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("owner", ownerId); @@ -125,11 +126,10 @@ public class JPAResourceStore implements ResourceStore { query.setParameter("serverId", resourceServerId); } - ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + StoreFactory storeFactory = provider.getStoreFactory(); query.getResultList().stream() - .map(id -> resourceStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(id -> new ResourceAdapter(id, entityManager, storeFactory)) .forEach(consumer); } @@ -247,17 +247,16 @@ public class JPAResourceStore implements ResourceStore { @Override public void findByScope(List scopes, String resourceServerId, Consumer consumer) { - TypedQuery query = entityManager.createNamedQuery("findResourceIdByScope", String.class); + TypedQuery query = entityManager.createNamedQuery("findResourceIdByScope", ResourceEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("scopeIds", scopes); query.setParameter("serverId", resourceServerId); - ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + StoreFactory storeFactory = provider.getStoreFactory(); query.getResultList().stream() - .map(id -> resourceStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(id -> new ResourceAdapter(id, entityManager, storeFactory)) .forEach(consumer); } @@ -268,16 +267,14 @@ public class JPAResourceStore implements ResourceStore { @Override public Resource findByName(String name, String ownerId, String resourceServerId) { - TypedQuery query = entityManager.createNamedQuery("findResourceIdByName", String.class); + TypedQuery query = entityManager.createNamedQuery("findResourceIdByName", ResourceEntity.class); - query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("name", name); query.setParameter("ownerId", ownerId); try { - String id = query.getSingleResult(); - return provider.getStoreFactory().getResourceStore().findById(id, resourceServerId); + return new ResourceAdapter(query.getSingleResult(), entityManager, provider.getStoreFactory()); } catch (NoResultException ex) { return null; } @@ -294,18 +291,17 @@ public class JPAResourceStore implements ResourceStore { @Override public void findByType(String type, String resourceServerId, Consumer consumer) { - TypedQuery query = entityManager.createNamedQuery("findResourceIdByType", String.class); + TypedQuery query = entityManager.createNamedQuery("findResourceIdByType", ResourceEntity.class); query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", type); query.setParameter("ownerId", resourceServerId); query.setParameter("serverId", resourceServerId); - ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + StoreFactory storeFactory = provider.getStoreFactory(); query.getResultList().stream() - .map(id -> resourceStore.findById(id, resourceServerId)) - .filter(Objects::nonNull) + .map(entity -> new ResourceAdapter(entity, entityManager, storeFactory)) .forEach(consumer); } } 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 98651dab38..a761370d1e 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 @@ -151,8 +151,7 @@ public class PolicyAdapter implements Policy, JpaModel { public Set getAssociatedPolicies() { Set result = new HashSet<>(); for (PolicyEntity policy : entity.getAssociatedPolicies()) { - Policy p = storeFactory.getPolicyStore().findById(policy.getId(), entity.getResourceServer().getId()); - result.add(p); + result.add(new PolicyAdapter(policy, em, storeFactory)); } return Collections.unmodifiableSet(result); } @@ -239,6 +238,8 @@ public class PolicyAdapter implements Policy, JpaModel { } } - - + @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 64f66db568..82735ed5cb 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 @@ -231,6 +231,11 @@ public class ResourceAdapter implements Resource, JpaModel { entity.getAttributes().removeAll(toRemove); } + @Override + public boolean isFetched(String association) { + return em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(this, association); + } + public static ResourceEntity toEntity(EntityManager em, Resource resource) { if (resource instanceof ResourceAdapter) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 2ee9ea5bb0..f9d51b8088 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -17,6 +17,7 @@ package org.keycloak.models.jpa; +import org.keycloak.authorization.jpa.entities.ResourceEntity; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; @@ -42,6 +43,7 @@ import org.keycloak.models.jpa.entities.FederatedIdentityEntity; import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity; import org.keycloak.models.jpa.entities.UserConsentEntity; import org.keycloak.models.jpa.entities.UserEntity; +import org.keycloak.models.jpa.entities.UserGroupMembershipEntity; import org.keycloak.models.utils.DefaultRoles; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.StorageId; @@ -50,6 +52,11 @@ import org.keycloak.storage.client.ClientStorageProvider; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -510,12 +517,9 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { @Override public UserModel getUserById(String id, RealmModel realm) { - TypedQuery query = em.createNamedQuery("getRealmUserById", UserEntity.class); - query.setParameter("id", id); - query.setParameter("realmId", realm.getId()); - List entities = query.getResultList(); - if (entities.size() == 0) return null; - return new UserAdapter(session, realm, em, entities.get(0)); + UserEntity userEntity = em.find(UserEntity.class, id); + if (userEntity == null) return null; + return new UserAdapter(session, realm, em, userEntity); } @Override @@ -700,55 +704,86 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { @Override public List searchForUser(Map attributes, RealmModel realm, int firstResult, int maxResults) { - StringBuilder builder = new StringBuilder("select u from UserEntity u where u.realmId = :realmId"); - for (Map.Entry entry : attributes.entrySet()) { - String attribute = null; - String parameterName = null; - if (entry.getKey().equals(UserModel.USERNAME)) { - attribute = "lower(u.username)"; - parameterName = JpaUserProvider.USERNAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.FIRST_NAME)) { - attribute = "lower(u.firstName)"; - parameterName = JpaUserProvider.FIRST_NAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.LAST_NAME)) { - attribute = "lower(u.lastName)"; - parameterName = JpaUserProvider.LAST_NAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.EMAIL)) { - attribute = "lower(u.email)"; - parameterName = JpaUserProvider.EMAIL; - } - if (attribute == null) continue; - builder.append(" and "); - builder.append(attribute).append(" like :").append(parameterName); + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery queryBuilder = builder.createQuery(UserEntity.class); + Root root = queryBuilder.from(UserEntity.class); + + List predicates = new ArrayList(); + + predicates.add(builder.equal(root.get("realmId"), realm.getId())); + + if (!session.getAttributeOrDefault(UserModel.INCLUDE_SERVICE_ACCOUNT, true)) { + predicates.add(root.get("serviceAccountClientLink").isNull()); } - builder.append(" order by u.username"); - String q = builder.toString(); - TypedQuery query = em.createQuery(q, UserEntity.class); - query.setParameter("realmId", realm.getId()); + for (Map.Entry entry : attributes.entrySet()) { - String parameterName = null; - if (entry.getKey().equals(UserModel.USERNAME)) { - parameterName = JpaUserProvider.USERNAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.FIRST_NAME)) { - parameterName = JpaUserProvider.FIRST_NAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.LAST_NAME)) { - parameterName = JpaUserProvider.LAST_NAME; - } else if (entry.getKey().equalsIgnoreCase(UserModel.EMAIL)) { - parameterName = JpaUserProvider.EMAIL; + String key = entry.getKey(); + String value = entry.getValue(); + + if (value == null) { + continue; + } + + switch (key) { + case UserModel.USERNAME: + case UserModel.FIRST_NAME: + case UserModel.LAST_NAME: + case UserModel.EMAIL: + predicates.add(builder.like(builder.lower(root.get(key)), "%" + value.toLowerCase() + "%")); } - if (parameterName == null) continue; - query.setParameter(parameterName, "%" + entry.getValue().toLowerCase() + "%"); } + + Set userGroups = (Set) session.getAttribute(UserModel.GROUPS); + + if (userGroups != null) { + Subquery subquery = queryBuilder.subquery(String.class); + Root from = subquery.from(UserGroupMembershipEntity.class); + + subquery.select(builder.literal(1)); + + List subPredicates = new ArrayList<>(); + + subPredicates.add(from.get("groupId").in(userGroups)); + subPredicates.add(builder.equal(from.get("user").get("id"), root.get("id"))); + + Subquery subquery1 = queryBuilder.subquery(String.class); + + subquery1.select(builder.literal(1)); + Root from1 = subquery1.from(ResourceEntity.class); + + List subs = new ArrayList<>(); + + subs.add(builder.like(from1.get("name"), builder.concat("group.resource.", from.get("groupId")))); + + subquery1.where(subs.toArray(new Predicate[subs.size()])); + + subPredicates.add(builder.exists(subquery1)); + + subquery.where(subPredicates.toArray(new Predicate[subPredicates.size()])); + + predicates.add(builder.exists(subquery)); + } + + queryBuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get(UserModel.USERNAME))); + + TypedQuery query = em.createQuery(queryBuilder); + if (firstResult != -1) { query.setFirstResult(firstResult); } + if (maxResults != -1) { query.setMaxResults(maxResults); } - List results = query.getResultList(); - List users = new ArrayList(); - for (UserEntity entity : results) users.add(new UserAdapter(session, realm, em, entity)); - return users; + + List results = new ArrayList<>(); + UserProvider users = session.users(); + + for (UserEntity entity : query.getResultList()) { + results.add(users.getUserById(entity.getId(), realm)); + } + + return results; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index f85816158f..cd12fc9258 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -46,7 +46,6 @@ import java.util.Collection; @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"), @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), - @NamedQuery(name="getRealmUserById", query="select u from UserEntity u where u.id = :id and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"), diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml new file mode 100644 index 0000000000..3569849f39 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml @@ -0,0 +1,27 @@ + + + + + + + + NAME LIKE 'group.resource.%' + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index f4f20037af..9a3e021fa4 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -59,4 +59,5 @@ + 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 0dafea9217..d9e1a0a5f4 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 @@ -163,4 +163,6 @@ 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 2cf2ea14ea..af8062b78b 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 @@ -180,4 +180,6 @@ public interface Resource { void setAttribute(String name, List values); void removeAttribute(String name); + + boolean isFetched(String association); } diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index 42f5d651a7..19773dcaf9 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -77,6 +77,15 @@ public interface KeycloakSession { Object getAttribute(String attribute); T getAttribute(String attribute, Class clazz); + default T getAttributeOrDefault(String attribute, T defaultValue) { + T value = (T) getAttribute(attribute); + + if (value == null) { + return defaultValue; + } + + return value; + } Object removeAttribute(String attribute); void setAttribute(String name, Object value); diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 91b3769873..3da65250ef 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -33,6 +33,8 @@ public interface UserModel extends RoleMapperModel { String FIRST_NAME = "firstName"; String EMAIL = "email"; String LOCALE = "locale"; + String INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account"; + String GROUPS = "keycloak.session.realm.users.query.groups"; interface UserRemovedEvent extends ProviderEvent { RealmModel getRealm(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index e8a57c75ef..cc2389a581 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -35,6 +35,8 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ForbiddenException; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator; +import org.keycloak.util.JsonSerialization; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -47,7 +49,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -55,6 +57,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; /** * Base resource for managing users @@ -181,12 +184,13 @@ public class UsersResource { @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, @QueryParam("briefRepresentation") Boolean briefRepresentation) { - auth.users().requireQuery(); + UserPermissionEvaluator userPermissionEvaluator = auth.users(); + + userPermissionEvaluator.requireQuery(); firstResult = firstResult != null ? firstResult : -1; maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; - List results = new ArrayList(); List userModels = Collections.emptyList(); if (search != null) { if (search.startsWith(SEARCH_ID_PARAMETER)) { @@ -198,7 +202,7 @@ public class UsersResource { userModels = session.users().searchForUser(search.trim(), realm, firstResult, maxResults); } } else if (last != null || first != null || email != null || username != null) { - Map attributes = new HashMap(); + Map attributes = new HashMap<>(); if (last != null) { attributes.put(UserModel.LAST_NAME, last); } @@ -211,22 +215,12 @@ public class UsersResource { if (username != null) { attributes.put(UserModel.USERNAME, username); } - userModels = session.users().searchForUser(attributes, realm, firstResult, maxResults); + return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, true); } else { - userModels = session.users().getUsers(realm, firstResult, maxResults, false); + return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false); } - boolean canViewGlobal = auth.users().canView(); - boolean briefRepresentationB = briefRepresentation != null && briefRepresentation; - for (UserModel user : userModels) { - if (!canViewGlobal && !auth.users().canView(user)) continue; - UserRepresentation userRep = briefRepresentationB - ? ModelToRepresentation.toBriefRepresentation(user) - : ModelToRepresentation.toRepresentation(session, realm, user); - userRep.setAccess(auth.users().getAccess(user)); - results.add(userRep); - } - return results; + return toRepresentation(realm, userPermissionEvaluator, briefRepresentation, userModels); } @Path("count") @@ -238,4 +232,42 @@ public class UsersResource { return session.users().getUsersCount(realm); } + + private List searchForUser(Map attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) { + session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts); + + if (!auth.users().canView()) { + Set groupModels = auth.groups().getGroupsWithViewPermission(); + + if (!groupModels.isEmpty()) { + session.setAttribute(UserModel.GROUPS, groupModels); + } + } + + List userModels = session.users().searchForUser(attributes, realm, firstResult, maxResults); + + return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels); + } + + private List toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, List userModels) { + boolean briefRepresentationB = briefRepresentation != null && briefRepresentation; + List results = new ArrayList<>(); + boolean canViewGlobal = usersEvaluator.canView(); + + usersEvaluator.grantIfNoPermission(session.getAttribute(UserModel.GROUPS) != null); + + for (UserModel user : userModels) { + if (!canViewGlobal) { + if (!usersEvaluator.canView(user)) { + continue; + } + } + UserRepresentation userRep = briefRepresentationB + ? ModelToRepresentation.toBriefRepresentation(user) + : ModelToRepresentation.toRepresentation(session, realm, user); + userRep.setAccess(usersEvaluator.getAccess(user)); + results.add(userRep); + } + return results; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java index 26ec3b5915..f6ab47e8c6 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java @@ -342,7 +342,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } }; - return root.evaluatePermission(resource, scope, server, context); + return root.evaluatePermission(resource, server, context, scope); } return true; } @@ -376,7 +376,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = manageScope(server); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override @@ -404,7 +404,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = configureScope(server); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override public void requireConfigure(ClientModel client) { @@ -450,7 +450,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = viewScope(server); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override @@ -529,7 +529,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = mapRolesScope(server); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override @@ -606,7 +606,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_COMPOSITE_SCOPE, server.getId()); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override public boolean canMapClientScopeRoles(ClientModel client) { @@ -628,7 +628,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM } Scope scope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_CLIENT_SCOPE, server.getId()); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, server, scope); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java index d3a5213627..831e9f0667 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java @@ -19,6 +19,7 @@ package org.keycloak.services.resources.admin.permissions; import org.keycloak.models.GroupModel; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -45,17 +46,17 @@ public interface GroupPermissionEvaluator { void requireView(); - boolean canViewMembers(GroupModel group); + boolean getGroupsWithViewPermission(GroupModel group); void requireViewMembers(GroupModel group); boolean canManageMembers(GroupModel group); - void requireManageMembers(GroupModel group); - boolean canManageMembership(GroupModel group); void requireManageMembership(GroupModel group); Map getAccess(GroupModel group); + + Set getGroupsWithViewPermission(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java index c2a46894f2..dabc02e8f6 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java @@ -16,21 +16,27 @@ */ package org.keycloak.services.resources.admin.permissions; -import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.AdminRoles; import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ForbiddenException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -39,45 +45,46 @@ import java.util.Set; * @version $Revision: 1 $ */ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManagement { - private static final Logger logger = Logger.getLogger(GroupPermissions.class); - public static final String MAP_ROLE_SCOPE = "map-role"; - public static final String MANAGE_MEMBERSHIP_SCOPE = "manage-membership"; - public static final String MANAGE_MEMBERS_SCOPE = "manage-members"; - public static final String VIEW_MEMBERS_SCOPE = "view-members"; - protected final KeycloakSession session; - protected final RealmModel realm; - protected final AuthorizationProvider authz; - protected final MgmtPermissions root; - public GroupPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) { - this.session = session; - this.realm = realm; + private static final String MANAGE_MEMBERSHIP_SCOPE = "manage-membership"; + private static final String MANAGE_MEMBERS_SCOPE = "manage-members"; + private static final String VIEW_MEMBERS_SCOPE = "view-members"; + private static final String RESOURCE_NAME_PREFIX = "group.resource."; + + private final AuthorizationProvider authz; + private final MgmtPermissions root; + private final ResourceStore resourceStore; + private final PolicyStore policyStore; + + GroupPermissions(AuthorizationProvider authz, MgmtPermissions root) { this.authz = authz; this.root = root; + resourceStore = authz.getStoreFactory().getResourceStore(); + policyStore = authz.getStoreFactory().getPolicyStore(); } private static String getGroupResourceName(GroupModel group) { - return "group.resource." + group.getId(); + return RESOURCE_NAME_PREFIX + group.getId(); } - public static String getManagePermissionGroup(GroupModel group) { + private static String getManagePermissionGroup(GroupModel group) { return "manage.permission.group." + group.getId(); } - public static String getManageMembersPermissionGroup(GroupModel group) { + private static String getManageMembersPermissionGroup(GroupModel group) { return "manage.members.permission.group." + group.getId(); } - public static String getManageMembershipPermissionGroup(GroupModel group) { + private static String getManageMembershipPermissionGroup(GroupModel group) { return "manage.membership.permission.group." + group.getId(); } - public static String getViewPermissionGroup(GroupModel group) { + private static String getViewPermissionGroup(GroupModel group) { return "view.permission.group." + group.getId(); } - public static String getViewMembersPermissionGroup(GroupModel group) { + private static String getViewMembersPermissionGroup(GroupModel group) { return "view.members.permission.group." + group.getId(); } @@ -92,9 +99,9 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag Scope manageMembershipScope = root.initializeRealmScope(MANAGE_MEMBERSHIP_SCOPE); String groupResourceName = getGroupResourceName(group); - Resource groupResource = authz.getStoreFactory().getResourceStore().findByName(groupResourceName, server.getId()); + Resource groupResource = resourceStore.findByName(groupResourceName, server.getId()); if (groupResource == null) { - groupResource = authz.getStoreFactory().getResourceStore().create(groupResourceName, server, server.getId()); + groupResource = resourceStore.create(groupResourceName, server, server.getId()); Set scopeset = new HashSet<>(); scopeset.add(manageScope); scopeset.add(viewScope); @@ -102,29 +109,30 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag scopeset.add(manageMembershipScope); scopeset.add(manageMembersScope); groupResource.updateScopes(scopeset); + groupResource.setType("Group"); } String managePermissionName = getManagePermissionGroup(group); - Policy managePermission = authz.getStoreFactory().getPolicyStore().findByName(managePermissionName, server.getId()); + Policy managePermission = policyStore.findByName(managePermissionName, server.getId()); if (managePermission == null) { Helper.addEmptyScopePermission(authz, server, managePermissionName, groupResource, manageScope); } String viewPermissionName = getViewPermissionGroup(group); - Policy viewPermission = authz.getStoreFactory().getPolicyStore().findByName(viewPermissionName, server.getId()); + Policy viewPermission = policyStore.findByName(viewPermissionName, server.getId()); if (viewPermission == null) { Helper.addEmptyScopePermission(authz, server, viewPermissionName, groupResource, viewScope); } String manageMembersPermissionName = getManageMembersPermissionGroup(group); - Policy manageMembersPermission = authz.getStoreFactory().getPolicyStore().findByName(manageMembersPermissionName, server.getId()); + Policy manageMembersPermission = policyStore.findByName(manageMembersPermissionName, server.getId()); if (manageMembersPermission == null) { Helper.addEmptyScopePermission(authz, server, manageMembersPermissionName, groupResource, manageMembersScope); } String viewMembersPermissionName = getViewMembersPermissionGroup(group); - Policy viewMembersPermission = authz.getStoreFactory().getPolicyStore().findByName(viewMembersPermissionName, server.getId()); + Policy viewMembersPermission = policyStore.findByName(viewMembersPermissionName, server.getId()); if (viewMembersPermission == null) { Helper.addEmptyScopePermission(authz, server, viewMembersPermissionName, groupResource, viewMembersScope); } String manageMembershipPermissionName = getManageMembershipPermissionGroup(group); - Policy manageMembershipPermission = authz.getStoreFactory().getPolicyStore().findByName(manageMembershipPermissionName, server.getId()); + Policy manageMembershipPermission = policyStore.findByName(manageMembershipPermissionName, server.getId()); if (manageMembershipPermission == null) { Helper.addEmptyScopePermission(authz, server, manageMembershipPermissionName, groupResource, manageMembershipScope); } @@ -143,21 +151,12 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag } } - - @Override public boolean isPermissionsEnabled(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return false; - return authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()) != null; - } - - private Resource groupResource(GroupModel group) { - ResourceServer server = root.realmResourceServer(); - if (server == null) return null; - String groupResourceName = getGroupResourceName(group); - return authz.getStoreFactory().getResourceStore().findByName(groupResourceName, server.getId()); + return resourceStore.findByName(getGroupResourceName(group), server.getId()) != null; } @Override @@ -169,78 +168,46 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag } } - private void deletePermissions(GroupModel group) { - ResourceServer server = root.realmResourceServer(); - if (server == null) return; - Policy managePermission = managePermission(group); - if (managePermission != null) { - authz.getStoreFactory().getPolicyStore().delete(managePermission.getId()); - } - Policy viewPermission = viewPermission(group); - if (viewPermission != null) { - authz.getStoreFactory().getPolicyStore().delete(viewPermission.getId()); - } - Policy manageMembersPermission = manageMembersPermission(group); - if (manageMembersPermission != null) { - authz.getStoreFactory().getPolicyStore().delete(manageMembersPermission.getId()); - } - Policy viewMembersPermission = viewMembersPermission(group); - if (viewMembersPermission != null) { - authz.getStoreFactory().getPolicyStore().delete(viewMembersPermission.getId()); - } - Policy manageMembershipPermission = manageMembershipPermission(group); - if (manageMembershipPermission != null) { - authz.getStoreFactory().getPolicyStore().delete(manageMembershipPermission.getId()); - } - Resource resource = groupResource(group); - if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId()); - } - @Override public Policy viewMembersPermission(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - String viewMembersPermissionName = getViewMembersPermissionGroup(group); - return authz.getStoreFactory().getPolicyStore().findByName(viewMembersPermissionName, server.getId()); + return policyStore.findByName(getViewMembersPermissionGroup(group), server.getId()); } @Override public Policy manageMembersPermission(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - String manageMembersPermissionName = getManageMembersPermissionGroup(group); - return authz.getStoreFactory().getPolicyStore().findByName(manageMembersPermissionName, server.getId()); + return policyStore.findByName(getManageMembersPermissionGroup(group), server.getId()); } @Override public Policy manageMembershipPermission(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - String manageMembershipPermissionName = getManageMembershipPermissionGroup(group); - return authz.getStoreFactory().getPolicyStore().findByName(manageMembershipPermissionName, server.getId()); + return policyStore.findByName(getManageMembershipPermissionGroup(group), server.getId()); } @Override public Policy viewPermission(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - String viewPermissionName = getViewPermissionGroup(group); - return authz.getStoreFactory().getPolicyStore().findByName(viewPermissionName, server.getId()); + return policyStore.findByName(getViewPermissionGroup(group), server.getId()); } @Override public Policy managePermission(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - String managePermissionName = getManagePermissionGroup(group); - return authz.getStoreFactory().getPolicyStore().findByName(managePermissionName, server.getId()); + return policyStore.findByName(getManagePermissionGroup(group), server.getId()); } @Override public Resource resource(GroupModel group) { ResourceServer server = root.realmResourceServer(); if (server == null) return null; - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); + Resource resource = resourceStore.findByName(getGroupResourceName(group), server.getId()); if (resource == null) return null; return resource; } @@ -257,35 +224,17 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag return scopes; } - - - @Override public boolean canManage(GroupModel group) { - if (canManage()) return true; + if (canManage()) { + return true; + } + if (!root.isAdminSameRealm()) { return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); - if (resource == null) return false; - - Policy policy = managePermission(group); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmManageScope(); - return root.evaluatePermission(resource, scope, server); + return hasPermission(group, MgmtPermissions.MANAGE_SCOPE); } @Override @@ -294,37 +243,18 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag throw new ForbiddenException(); } } + @Override public boolean canView(GroupModel group) { - return hasView(group) || canManage(group); - } - - private boolean hasView(GroupModel group) { - if (canView()) return true; + if (canView() || canManage()) { + return true; + } if (!root.isAdminSameRealm()) { return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); - if (resource == null) return false; - - Policy policy = viewPermission(group); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then abort - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmViewScope(); - return root.evaluatePermission(resource, scope, server); + return hasPermission(group, MgmtPermissions.VIEW_SCOPE, MgmtPermissions.MANAGE_SCOPE); } @Override @@ -357,15 +287,11 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag } } - - @Override - public boolean canViewMembers(GroupModel group) { - return canViewMembersEvaluation(group) || canManageMembers(group); - } - - private boolean canViewMembersEvaluation(GroupModel group) { - if (root.users().canView()) return true; + public boolean getGroupsWithViewPermission(GroupModel group) { + if (root.users().canView() || root.users().canManage()) { + return true; + } if (!root.isAdminSameRealm()) { return false; @@ -374,34 +300,41 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag ResourceServer server = root.realmResourceServer(); if (server == null) return false; - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); - if (resource == null) return false; - - Policy policy = viewMembersPermission(group); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = authz.getStoreFactory().getScopeStore().findByName(VIEW_MEMBERS_SCOPE, server.getId()); - - return root.evaluatePermission(resource, scope, server); + return hasPermission(group, VIEW_MEMBERS_SCOPE, MANAGE_MEMBERS_SCOPE); } + @Override + public Set getGroupsWithViewPermission() { + if (root.users().canView() || root.users().canManage()) return Collections.emptySet(); + + if (!root.isAdminSameRealm()) { + return Collections.emptySet(); + } + + ResourceServer server = root.realmResourceServer(); + + if (server == null) { + return Collections.emptySet(); + } + + Set granted = new HashSet<>(); + + resourceStore.findByType("Group", server.getId(), resource -> { + if (hasPermission(resource, null, VIEW_MEMBERS_SCOPE, MANAGE_MEMBERS_SCOPE)) { + granted.add(resource.getName().substring(RESOURCE_NAME_PREFIX.length())); + } + }); + + return granted; + } @Override public void requireViewMembers(GroupModel group) { - if (!canViewMembers(group)) { + if (!getGroupsWithViewPermission(group)) { throw new ForbiddenException(); } } - @Override public boolean canManageMembers(GroupModel group) { if (root.users().canManage()) return true; @@ -413,29 +346,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag ResourceServer server = root.realmResourceServer(); if (server == null) return false; - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); - if (resource == null) return false; - - Policy policy = manageMembersPermission(group); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = authz.getStoreFactory().getScopeStore().findByName(MANAGE_MEMBERS_SCOPE, server.getId()); - return root.evaluatePermission(resource, scope, server); - } - - @Override - public void requireManageMembers(GroupModel group) { - if (!canManageMembers(group)) { - throw new ForbiddenException(); - } + return hasPermission(group, MANAGE_MEMBERS_SCOPE); } @Override @@ -446,25 +357,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(getGroupResourceName(group), server.getId()); - if (resource == null) return false; - - Policy policy = manageMembershipPermission(group); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = authz.getStoreFactory().getScopeStore().findByName(MANAGE_MEMBERSHIP_SCOPE, server.getId()); - return root.evaluatePermission(resource, scope, server); + return hasPermission(group, MANAGE_MEMBERSHIP_SCOPE); } @Override @@ -483,7 +376,81 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag return map; } + private boolean hasPermission(GroupModel group, String... scopes) { + return hasPermission(group, null, scopes); + } + + private boolean hasPermission(GroupModel group, EvaluationContext context, String... scopes) { + ResourceServer server = root.realmResourceServer(); + + if (server == null) { + return false; + } + + Resource resource = resourceStore.findByName(getGroupResourceName(group), server.getId()); + + if (resource == null) { + return false; + } + + return hasPermission(resource, context, scopes); + } + + private boolean hasPermission(Resource resource, EvaluationContext context, String... scopes) { + ResourceServer server = root.realmResourceServer(); + Collection permissions; + + if (context == null) { + permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server); + } else { + permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server, context); + } + + List expectedScopes = Arrays.asList(scopes); + for (Permission permission : permissions) { + for (String scope : permission.getScopes()) { + if (expectedScopes.contains(scope)) { + return true; + } + } + } + return false; + } + + private Resource groupResource(GroupModel group) { + ResourceServer server = root.realmResourceServer(); + if (server == null) return null; + String groupResourceName = getGroupResourceName(group); + return resourceStore.findByName(groupResourceName, server.getId()); + } + + private void deletePermissions(GroupModel group) { + ResourceServer server = root.realmResourceServer(); + if (server == null) return; + Policy managePermission = managePermission(group); + if (managePermission != null) { + policyStore.delete(managePermission.getId()); + } + Policy viewPermission = viewPermission(group); + if (viewPermission != null) { + policyStore.delete(viewPermission.getId()); + } + Policy manageMembersPermission = manageMembersPermission(group); + if (manageMembersPermission != null) { + policyStore.delete(manageMembersPermission.getId()); + } + Policy viewMembersPermission = viewMembersPermission(group); + if (viewMembersPermission != null) { + policyStore.delete(viewMembersPermission.getId()); + } + Policy manageMembershipPermission = manageMembershipPermission(group); + if (manageMembershipPermission != null) { + policyStore.delete(manageMembershipPermission.getId()); + } + Resource resource = groupResource(group); + if (resource != null) resourceStore.delete(resource.getId()); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java index a0e2e44992..db1a96a951 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java @@ -189,7 +189,7 @@ class IdentityProviderPermissions implements IdentityProviderPermissionManageme } }; - return root.evaluatePermission(resource, scope, server, context); + return root.evaluatePermission(resource, server, context, scope); } return true; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java index 735dd48286..56e5f5316f 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java @@ -28,10 +28,8 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.permission.ResourcePermission; -import org.keycloak.authorization.permission.evaluator.PermissionEvaluator; import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.authorization.store.ResourceServerStore; -import org.keycloak.authorization.util.Permissions; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -39,11 +37,14 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminAuth; import java.util.Arrays; +import java.util.Collection; +import java.util.List; /** * @author Bill Burke @@ -205,7 +206,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage @Override public UserPermissions users() { if (users != null) return users; - users = new UserPermissions(session, realm, authz, this); + users = new UserPermissions(session, authz, this); return users; } @@ -233,7 +234,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage @Override public GroupPermissions groups() { if (groups != null) return groups; - groups = new GroupPermissions(session, realm, authz, this); + groups = new GroupPermissions(authz, this); return groups; } @@ -313,25 +314,36 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage return authz.getStoreFactory().getScopeStore().findByName(scope, server.getId()); } - public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer) { + public boolean evaluatePermission(Resource resource, ResourceServer resourceServer, Scope... scope) { Identity identity = identity(); if (identity == null) { throw new RuntimeException("Identity of admin is not set for permission query"); } - return evaluatePermission(resource, scope, resourceServer, identity); + return evaluatePermission(resource, resourceServer, identity, scope); } - public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, Identity identity) { + public Collection evaluatePermission(ResourcePermission permission, ResourceServer resourceServer) { + return evaluatePermission(permission, resourceServer, new DefaultEvaluationContext(identity, session)); + } + + public Collection evaluatePermission(ResourcePermission permission, ResourceServer resourceServer, EvaluationContext context) { + return evaluatePermission(Arrays.asList(permission), resourceServer, context); + } + + public boolean evaluatePermission(Resource resource, ResourceServer resourceServer, Identity identity, Scope... scope) { EvaluationContext context = new DefaultEvaluationContext(identity, session); - return evaluatePermission(resource, scope, resourceServer, context); + return evaluatePermission(resource, resourceServer, context, scope); } - public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, EvaluationContext context) { + public boolean evaluatePermission(Resource resource, ResourceServer resourceServer, EvaluationContext context, Scope... scope) { + return !evaluatePermission(Arrays.asList(new ResourcePermission(resource, Arrays.asList(scope), resourceServer)), resourceServer, context).isEmpty(); + } + + public Collection evaluatePermission(List permissions, ResourceServer resourceServer, EvaluationContext context) { RealmModel oldRealm = session.getContext().getRealm(); try { session.getContext().setRealm(realm); - ResourcePermission permission = Permissions.permission(resourceServer, resource, scope); - return !authz.evaluators().from(Arrays.asList(permission), context).evaluate(resourceServer, null).isEmpty(); + return authz.evaluators().from(permissions, context).evaluate(resourceServer, null); } finally { session.getContext().setRealm(oldRealm); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java index 464f512b42..5ef81005b4 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java @@ -307,7 +307,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme Resource roleResource = resource(role); Scope mapRoleScope = mapRoleScope(resourceServer); - if (root.evaluatePermission(roleResource, mapRoleScope, resourceServer)) { + if (root.evaluatePermission(roleResource, resourceServer, mapRoleScope)) { return checkAdminRoles(role); } else { return false; @@ -391,7 +391,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme Resource roleResource = resource(role); Scope scope = mapCompositeScope(resourceServer); - if (root.evaluatePermission(roleResource, scope, resourceServer)) { + if (root.evaluatePermission(roleResource, resourceServer, scope)) { return checkAdminRoles(role); } else { return false; @@ -430,7 +430,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme Resource roleResource = resource(role); Scope scope = mapClientScope(resourceServer); - return root.evaluatePermission(roleResource, scope, resourceServer); + return root.evaluatePermission(roleResource, resourceServer, scope); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java index f0e2a86633..c9a7e48f42 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java @@ -16,7 +16,6 @@ */ package org.keycloak.services.resources.admin.permissions; -import org.keycloak.authorization.model.Policy; import org.keycloak.models.UserModel; import java.util.Map; @@ -26,42 +25,30 @@ import java.util.Map; * @version $Revision: 1 $ */ public interface UserPermissionEvaluator { - boolean canManage(); - void requireManage(); - - boolean canManage(UserModel user); void requireManage(UserModel user); - - boolean canQuery(); + boolean canManage(); + boolean canManage(UserModel user); void requireQuery(); - - boolean canQuery(UserModel user); - - void requireQuery(UserModel user); - - boolean canView(); - boolean canView(UserModel user); - void requireView(UserModel user); + boolean canQuery(); void requireView(); - - boolean canImpersonate(UserModel user); - - boolean isImpersonatable(UserModel user); - - boolean canImpersonate(); + void requireView(UserModel user); + boolean canView(); + boolean canView(UserModel user); void requireImpersonate(UserModel user); + boolean canImpersonate(); + boolean canImpersonate(UserModel user); + boolean isImpersonatable(UserModel user); Map getAccess(UserModel user); + void requireMapRoles(UserModel user); boolean canMapRoles(UserModel user); - void requireMapRoles(UserModel user); - - boolean canManageGroupMembership(UserModel user); - void requireManageGroupMembership(UserModel user); + boolean canManageGroupMembership(UserModel user); + void grantIfNoPermission(boolean grantIfNoPermission); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java index a8d9c8ba87..92d9aa5165 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java @@ -16,7 +16,6 @@ */ package org.keycloak.services.resources.admin.permissions; -import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.common.ClientModelIdentity; import org.keycloak.authorization.common.DefaultEvaluationContext; @@ -26,23 +25,30 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.ImpersonationConstants; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ForbiddenException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; /** * Manages default policies for all users. @@ -52,28 +58,32 @@ import java.util.Set; * @version $Revision: 1 $ */ class UserPermissions implements UserPermissionEvaluator, UserPermissionManagement { - private static final Logger logger = Logger.getLogger(UserPermissions.class); - public static final String MAP_ROLES_SCOPE="map-roles"; - public static final String IMPERSONATE_SCOPE="impersonate"; - public static final String USER_IMPERSONATED_SCOPE="user-impersonated"; - public static final String MANAGE_GROUP_MEMBERSHIP_SCOPE="manage-group-membership"; - public static final String MAP_ROLES_PERMISSION_USERS = "map-roles.permission.users"; - public static final String ADMIN_IMPERSONATING_PERMISSION = "admin-impersonating.permission.users"; - public static final String USER_IMPERSONATED_PERMISSION = "user-impersonated.permission.users"; - public static final String MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS = "manage-group-membership.permission.users"; - public static final String MANAGE_PERMISSION_USERS = "manage.permission.users"; - public static final String VIEW_PERMISSION_USERS = "view.permission.users"; - public static final String USERS_RESOURCE = "Users"; - protected final KeycloakSession session; - protected final RealmModel realm; - protected final AuthorizationProvider authz; - protected final MgmtPermissions root; - public UserPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) { + private static final String MAP_ROLES_SCOPE="map-roles"; + private static final String IMPERSONATE_SCOPE="impersonate"; + private static final String USER_IMPERSONATED_SCOPE="user-impersonated"; + private static final String MANAGE_GROUP_MEMBERSHIP_SCOPE="manage-group-membership"; + private static final String MAP_ROLES_PERMISSION_USERS = "map-roles.permission.users"; + private static final String ADMIN_IMPERSONATING_PERMISSION = "admin-impersonating.permission.users"; + private static final String USER_IMPERSONATED_PERMISSION = "user-impersonated.permission.users"; + private static final String MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS = "manage-group-membership.permission.users"; + private static final String MANAGE_PERMISSION_USERS = "manage.permission.users"; + private static final String VIEW_PERMISSION_USERS = "view.permission.users"; + private static final String USERS_RESOURCE = "Users"; + + private final KeycloakSession session; + private final AuthorizationProvider authz; + private final MgmtPermissions root; + private final PolicyStore policyStore; + private final ResourceStore resourceStore; + private boolean grantIfNoPermission = false; + + UserPermissions(KeycloakSession session, AuthorizationProvider authz, MgmtPermissions root) { this.session = session; - this.realm = realm; this.authz = authz; this.root = root; + policyStore = authz.getStoreFactory().getPolicyStore(); + resourceStore = authz.getStoreFactory().getResourceStore(); } @@ -88,9 +98,9 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme Scope userImpersonatedScope = root.initializeRealmScope(USER_IMPERSONATED_SCOPE); Scope manageGroupMembershipScope = root.initializeRealmScope(MANAGE_GROUP_MEMBERSHIP_SCOPE); - Resource usersResource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); + Resource usersResource = resourceStore.findByName(USERS_RESOURCE, server.getId()); if (usersResource == null) { - usersResource = authz.getStoreFactory().getResourceStore().create(USERS_RESOURCE, server, server.getId()); + usersResource = resourceStore.create(USERS_RESOURCE, server, server.getId()); Set scopeset = new HashSet<>(); scopeset.add(manageScope); scopeset.add(viewScope); @@ -100,27 +110,27 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme scopeset.add(userImpersonatedScope); usersResource.updateScopes(scopeset); } - Policy managePermission = authz.getStoreFactory().getPolicyStore().findByName(MANAGE_PERMISSION_USERS, server.getId()); + Policy managePermission = policyStore.findByName(MANAGE_PERMISSION_USERS, server.getId()); if (managePermission == null) { Helper.addEmptyScopePermission(authz, server, MANAGE_PERMISSION_USERS, usersResource, manageScope); } - Policy viewPermission = authz.getStoreFactory().getPolicyStore().findByName(VIEW_PERMISSION_USERS, server.getId()); + Policy viewPermission = policyStore.findByName(VIEW_PERMISSION_USERS, server.getId()); if (viewPermission == null) { Helper.addEmptyScopePermission(authz, server, VIEW_PERMISSION_USERS, usersResource, viewScope); } - Policy mapRolesPermission = authz.getStoreFactory().getPolicyStore().findByName(MAP_ROLES_PERMISSION_USERS, server.getId()); + Policy mapRolesPermission = policyStore.findByName(MAP_ROLES_PERMISSION_USERS, server.getId()); if (mapRolesPermission == null) { Helper.addEmptyScopePermission(authz, server, MAP_ROLES_PERMISSION_USERS, usersResource, mapRolesScope); } - Policy membershipPermission = authz.getStoreFactory().getPolicyStore().findByName(MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, server.getId()); + Policy membershipPermission = policyStore.findByName(MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, server.getId()); if (membershipPermission == null) { Helper.addEmptyScopePermission(authz, server, MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, usersResource, manageGroupMembershipScope); } - Policy impersonatePermission = authz.getStoreFactory().getPolicyStore().findByName(ADMIN_IMPERSONATING_PERMISSION, server.getId()); + Policy impersonatePermission = policyStore.findByName(ADMIN_IMPERSONATING_PERMISSION, server.getId()); if (impersonatePermission == null) { Helper.addEmptyScopePermission(authz, server, ADMIN_IMPERSONATING_PERMISSION, usersResource, impersonateScope); } - impersonatePermission = authz.getStoreFactory().getPolicyStore().findByName(USER_IMPERSONATED_PERMISSION, server.getId()); + impersonatePermission = policyStore.findByName(USER_IMPERSONATED_PERMISSION, server.getId()); if (impersonatePermission == null) { Helper.addEmptyScopePermission(authz, server, USER_IMPERSONATED_PERMISSION, usersResource, userImpersonatedScope); } @@ -144,7 +154,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme ResourceServer server = root.realmResourceServer(); if (server == null) return false; - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); + Resource resource = resourceStore.findByName(USERS_RESOURCE, server.getId()); if (resource == null) return false; Policy policy = managePermission(); @@ -161,45 +171,6 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } } - private void deletePermissionSetup() { - ResourceServer server = root.realmResourceServer(); - if (server == null) return; - Policy policy = managePermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - policy = viewPermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - policy = mapRolesPermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - policy = manageGroupMembershipPermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - policy = adminImpersonatingPermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - policy = userImpersonatedPermission(); - if (policy != null) { - authz.getStoreFactory().getPolicyStore().delete(policy.getId()); - - } - Resource usersResource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (usersResource != null) { - authz.getStoreFactory().getResourceStore().delete(usersResource.getId()); - } - } - public boolean canManageDefault() { return root.hasOneAdminRole(AdminRoles.MANAGE_USERS); } @@ -209,48 +180,40 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme ResourceServer server = root.realmResourceServer(); if (server == null) return null; - return authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); + return resourceStore.findByName(USERS_RESOURCE, server.getId()); } @Override public Policy managePermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(MANAGE_PERMISSION_USERS, server.getId()); + return policyStore.findByName(MANAGE_PERMISSION_USERS, root.realmResourceServer().getId()); } @Override public Policy viewPermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(VIEW_PERMISSION_USERS, server.getId()); + return policyStore.findByName(VIEW_PERMISSION_USERS, root.realmResourceServer().getId()); } @Override public Policy manageGroupMembershipPermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, server.getId()); + return policyStore.findByName(MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, root.realmResourceServer().getId()); } @Override public Policy mapRolesPermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(MAP_ROLES_PERMISSION_USERS, server.getId()); + return policyStore.findByName(MAP_ROLES_PERMISSION_USERS, root.realmResourceServer().getId()); } @Override public Policy adminImpersonatingPermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(ADMIN_IMPERSONATING_PERMISSION, server.getId()); + return policyStore.findByName(ADMIN_IMPERSONATING_PERMISSION, root.realmResourceServer().getId()); } @Override public Policy userImpersonatedPermission() { - ResourceServer server = root.realmResourceServer(); - return authz.getStoreFactory().getPolicyStore().findByName(USER_IMPERSONATED_PERMISSION, server.getId()); + return policyStore.findByName(USER_IMPERSONATED_PERMISSION, root.realmResourceServer().getId()); } - - /** * Is admin allowed to manage all users? In Authz terms, does the admin have the "manage" scope for the Users Authz resource? * @@ -266,31 +229,15 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme */ @Override public boolean canManage() { - if (canManageDefault()) return true; + if (canManageDefault()) { + return true; + } + if (!root.isAdminSameRealm()) { return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return false; - - Policy policy = authz.getStoreFactory().getPolicyStore().findByName(MANAGE_PERMISSION_USERS, server.getId()); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmManageScope(); - return root.evaluatePermission(resource, scope, server); - + return hasPermission(MgmtPermissions.MANAGE_SCOPE); } @Override @@ -319,63 +266,6 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } } - private interface EvaluateGroup { - boolean evaluate(GroupModel group); - } - - private boolean evaluateGroups(UserModel user, EvaluateGroup eval) { - for (GroupModel group : user.getGroups()) { - if (eval.evaluate(group)) return true; - } - return false; - } - - private boolean evaluateHierarchy(UserModel user, EvaluateGroup eval) { - Set visited = new HashSet<>(); - for (GroupModel group : user.getGroups()) { - if (evaluateHierarchy(eval, group, visited)) return true; - } - return false; - } - - private boolean evaluateHierarchy(EvaluateGroup eval, GroupModel group, Set visited) { - if (visited.contains(group)) return false; - if (eval.evaluate(group)) { - return true; - } - visited.add(group); - if (group.getParent() == null) return false; - return evaluateHierarchy(eval, group.getParent(), visited); - } - - private boolean canManageByGroup(UserModel user) { - /* no inheritance - return evaluateGroups(user, - (group) -> root.groups().canViewMembers(group) - ); - */ - - /* inheritance - */ - return evaluateHierarchy(user, (group) -> root.groups().canManageMembers(group)); - - } - private boolean canViewByGroup(UserModel user) { - /* no inheritance - return evaluateGroups(user, - (group) -> root.groups().canViewMembers(group) - ); - */ - - /* inheritance - */ - return evaluateHierarchy(user, (group) -> root.groups().canViewMembers(group)); - } - - public boolean canViewDefault() { - return root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS); - } - @Override public boolean canQuery() { return canView() || root.hasOneAdminRole(AdminRoles.QUERY_USERS); @@ -388,21 +278,6 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } } - @Override - public boolean canQuery(UserModel user) { - return canView(user); - } - - @Override - public void requireQuery(UserModel user) { - if (!canQuery(user)) { - throw new ForbiddenException(); - } - - } - - - /** * Is admin allowed to view all users? In Authz terms, does the admin have the "view" scope for the Users Authz resource? * @@ -418,34 +293,15 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme */ @Override public boolean canView() { - if (canViewDefault()) return true; + if (canViewDefault() || canManageDefault()) { + return true; + } + if (!root.isAdminSameRealm()) { return false; } - return hasViewPermission() || canManage(); - } - - private boolean hasViewPermission() { - ResourceServer server = root.realmResourceServer(); - if (server == null) return canViewDefault(); - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return canViewDefault(); - - Policy policy = authz.getStoreFactory().getPolicyStore().findByName(VIEW_PERMISSION_USERS, server.getId()); - if (policy == null) { - return canViewDefault(); - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return canViewDefault(); - } - - Scope scope = root.realmViewScope(); - return root.evaluatePermission(resource, scope, server); + return hasPermission(MgmtPermissions.VIEW_SCOPE, MgmtPermissions.MANAGE_SCOPE); } /** @@ -505,15 +361,20 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme @Override public boolean isImpersonatable(UserModel user) { - Identity userIdentity = new UserModelIdentity(root.realm, user); - ResourceServer server = root.realmResourceServer(); - if (server == null) return true; - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return true; + if (server == null) { + return true; + } + + Resource resource = resourceStore.findByName(USERS_RESOURCE, server.getId()); + + if (resource == null) { + return true; + } Policy policy = authz.getStoreFactory().getPolicyStore().findByName(USER_IMPERSONATED_PERMISSION, server.getId()); + if (policy == null) { return true; } @@ -524,8 +385,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme return true; } - Scope scope = root.realmScope(USER_IMPERSONATED_SCOPE); - return root.evaluatePermission(resource, scope, server, userIdentity); + return hasPermission(new DefaultEvaluationContext(new UserModelIdentity(root.realm, user), session), USER_IMPERSONATED_SCOPE); } @Override @@ -538,31 +398,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme return false; } - EvaluationContext context = new DefaultEvaluationContext(identity, session); - return canImpersonate(context); - } - - protected boolean canImpersonate(EvaluationContext context) { - - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return false; - - Policy policy = authz.getStoreFactory().getPolicyStore().findByName(ADMIN_IMPERSONATING_PERMISSION, server.getId()); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmScope(IMPERSONATE_SCOPE); - return root.evaluatePermission(resource, scope, server, context); + return canImpersonate(new DefaultEvaluationContext(identity, session)); } @Override @@ -591,25 +427,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; - - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return false; - - Policy policy = authz.getStoreFactory().getPolicyStore().findByName(MAP_ROLES_PERMISSION_USERS, server.getId()); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmScope(MAP_ROLES_SCOPE); - return root.evaluatePermission(resource, scope, server); + return hasPermission(MAP_ROLES_SCOPE); } @@ -621,7 +439,6 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } - @Override public boolean canManageGroupMembership(UserModel user) { if (canManage(user)) return true; @@ -630,26 +447,13 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme return false; } - ResourceServer server = root.realmResourceServer(); - if (server == null) return false; + return hasPermission(MANAGE_GROUP_MEMBERSHIP_SCOPE); - Resource resource = authz.getStoreFactory().getResourceStore().findByName(USERS_RESOURCE, server.getId()); - if (resource == null) return false; - - Policy policy = authz.getStoreFactory().getPolicyStore().findByName(MANAGE_GROUP_MEMBERSHIP_PERMISSION_USERS, server.getId()); - if (policy == null) { - return false; - } - - Set associatedPolicies = policy.getAssociatedPolicies(); - // if no policies attached to permission then just do default behavior - if (associatedPolicies == null || associatedPolicies.isEmpty()) { - return false; - } - - Scope scope = root.realmScope(MANAGE_GROUP_MEMBERSHIP_SCOPE); - return root.evaluatePermission(resource, scope, server); + } + @Override + public void grantIfNoPermission(boolean grantIfNoPermission) { + this.grantIfNoPermission = grantIfNoPermission; } @Override @@ -660,9 +464,113 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } + private boolean hasPermission(String... scopes) { + return hasPermission(null, scopes); + } + private boolean hasPermission(EvaluationContext context, String... scopes) { + ResourceServer server = root.realmResourceServer(); + if (server == null) { + return false; + } + Resource resource = resourceStore.findByName(USERS_RESOURCE, server.getId()); + List expectedScopes = Arrays.asList(scopes); + if (resource == null) { + return grantIfNoPermission && expectedScopes.contains(MgmtPermissions.MANAGE_SCOPE) && expectedScopes.contains(MgmtPermissions.VIEW_SCOPE); + } + Collection permissions; + + if (context == null) { + permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server); + } else { + permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server, context); + } + + for (Permission permission : permissions) { + for (String scope : permission.getScopes()) { + if (expectedScopes.contains(scope)) { + return true; + } + } + } + + return false; + } + + private void deletePermissionSetup() { + ResourceServer server = root.realmResourceServer(); + if (server == null) return; + Policy policy = managePermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + policy = viewPermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + policy = mapRolesPermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + policy = manageGroupMembershipPermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + policy = adminImpersonatingPermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + policy = userImpersonatedPermission(); + if (policy != null) { + policyStore.delete(policy.getId()); + + } + Resource usersResource = resourceStore.findByName(USERS_RESOURCE, server.getId()); + if (usersResource != null) { + resourceStore.delete(usersResource.getId()); + } + } + + private boolean canImpersonate(EvaluationContext context) { + return hasPermission(context, IMPERSONATE_SCOPE); + } + + private boolean evaluateHierarchy(UserModel user, Predicate eval) { + Set visited = new HashSet<>(); + for (GroupModel group : user.getGroups()) { + if (evaluateHierarchy(eval, group, visited)) return true; + } + return false; + } + + private boolean evaluateHierarchy(Predicate eval, GroupModel group, Set visited) { + if (visited.contains(group)) return false; + if (eval.test(group)) { + return true; + } + visited.add(group); + if (group.getParent() == null) return false; + return evaluateHierarchy(eval, group.getParent(), visited); + } + + private boolean canManageByGroup(UserModel user) { + return evaluateHierarchy(user, (group) -> root.groups().canManageMembers(group)); + + } + private boolean canViewByGroup(UserModel user) { + return evaluateHierarchy(user, (group) -> root.groups().getGroupsWithViewPermission(group)); + } + + public boolean canViewDefault() { + return root.hasOneAdminRole(AdminRoles.MANAGE_USERS, AdminRoles.VIEW_USERS); + } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java index 0d01c41496..b4ec0244c6 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java @@ -190,7 +190,6 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP @Override public List searchForUser(Map attributes, RealmModel realm, int firstResult, int maxResults) { - if (attributes.size() != 1) return Collections.EMPTY_LIST; String username = attributes.get(UserModel.USERNAME); if (username == null) return Collections.EMPTY_LIST; return searchForUser(username, realm, firstResult, maxResults); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java index 4b144fe14f..9c18c28db3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java @@ -16,16 +16,19 @@ */ package org.keycloak.testsuite.admin; +import org.hamcrest.Matchers; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Test; import org.keycloak.admin.client.Keycloak; +import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Resource; import org.keycloak.client.admin.cli.util.ConfigUtil; import org.keycloak.common.Profile; import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; import org.keycloak.models.GroupModel; import org.keycloak.representations.idm.ClientScopeRepresentation; @@ -41,6 +44,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.services.resources.admin.permissions.GroupPermissionManagement; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; @@ -898,6 +902,92 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { Assert.assertNotNull(client.realm("master").roles().get("offline_access")); } + @Test + public void testUserPagination() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + session.getContext().setRealm(realm); + + GroupModel customerAGroup = session.realms().createGroup(realm, "Customer A"); + UserModel customerAManager = session.users().addUser(realm, "customer-a-manager"); + session.userCredentialManager().updateCredential(realm, customerAManager, UserCredentialModel.password("password")); + customerAManager.joinGroup(customerAGroup); + ClientModel realmAdminClient = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID); + customerAManager.grantRole(realmAdminClient.getRole(AdminRoles.QUERY_USERS)); + customerAManager.setEnabled(true); + UserModel regularAdminUser = session.users().addUser(realm, "regular-admin-user"); + session.userCredentialManager().updateCredential(realm, regularAdminUser, UserCredentialModel.password("password")); + regularAdminUser.grantRole(realmAdminClient.getRole(AdminRoles.VIEW_USERS)); + regularAdminUser.setEnabled(true); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); + + GroupPermissionManagement groupPermission = management.groups(); + + groupPermission.setPermissionsEnabled(customerAGroup, true); + + UserPolicyRepresentation userPolicyRepresentation = new UserPolicyRepresentation(); + + userPolicyRepresentation.setName("Only " + customerAManager.getUsername()); + userPolicyRepresentation.addUser(customerAManager.getId()); + + Policy policy = groupPermission.viewMembersPermission(customerAGroup); + + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + + Policy userPolicy = provider.getStoreFactory().getPolicyStore().create(userPolicyRepresentation, management.realmResourceServer()); + + policy.addAssociatedPolicy(RepresentationToModel.toModel(userPolicyRepresentation, provider, userPolicy)); + + for (int i = 0; i < 20; i++) { + UserModel userModel = session.users().addUser(realm, "a" + i); + userModel.setFirstName("test"); + } + + for (int i = 20; i < 40; i++) { + UserModel userModel = session.users().addUser(realm, "b" + i); + userModel.setFirstName("test"); + userModel.joinGroup(customerAGroup); + } + }); + + Keycloak client = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth", + "test", "customer-a-manager", "password", Constants.ADMIN_CLI_CLIENT_ID); + + List result = client.realm("test").users().search(null, "test", null, null, -1, 20); + + Assert.assertEquals(20, result.size()); + Assert.assertThat(result, Matchers.everyItem(Matchers.hasProperty("username", Matchers.startsWith("b")))); + + result = client.realm("test").users().search(null, "test", null, null, 20, 40); + + Assert.assertEquals(0, result.size()); + + client = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth", + "test", "regular-admin-user", "password", Constants.ADMIN_CLI_CLIENT_ID); + + result = client.realm("test").users().search(null, "test", null, null, -1, 20); + + Assert.assertEquals(20, result.size()); + Assert.assertThat(result, Matchers.everyItem(Matchers.hasProperty("username", Matchers.startsWith("a")))); + + client.realm("test").users().search(null, null, null, null, -1, -1); + + Assert.assertEquals(20, result.size()); + Assert.assertThat(result, Matchers.everyItem(Matchers.hasProperty("username", Matchers.startsWith("a")))); + + client = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth", + "test", "customer-a-manager", "password", Constants.ADMIN_CLI_CLIENT_ID); + + result = client.realm("test").users().search(null, null, null, null, -1, 20); + + Assert.assertEquals(20, result.size()); + Assert.assertThat(result, Matchers.everyItem(Matchers.hasProperty("username", Matchers.startsWith("b")))); + + result = client.realm("test").users().search("a", -1, 20, false); + + Assert.assertEquals(0, result.size()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 52b42fc432..4017b2d160 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -223,6 +223,24 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { } } + protected void testMigrationTo4_6_0(boolean supportsAuthzService, boolean checkMigrationData) { + if (supportsAuthzService && checkMigrationData) { + testGroupPolicyTypeFineGrainedAdminPermission(); + } + } + + private void testGroupPolicyTypeFineGrainedAdminPermission() { + ClientsResource clients = migrationRealm.clients(); + ClientRepresentation clientRepresentation = clients.findByClientId("realm-management").get(0); + List resources = clients.get(clientRepresentation.getId()).authorization().resources().resources(); + + assertEquals(5, resources.size()); + + for (ResourceRepresentation resource : resources) { + assertEquals("Group", resource.getType()); + } + } + private void testCliConsoleScopeSize(RealmResource realm) { ClientRepresentation cli = realm.clients().findByClientId(Constants.ADMIN_CLI_CLIENT_ID).get(0); ClientRepresentation console = realm.clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0); @@ -543,12 +561,13 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMigrationTo3_4_2(); } - protected void testMigrationTo4_x(boolean supportsAuthzServices) { + protected void testMigrationTo4_x(boolean supportsAuthzServices, boolean checkMigrationData) { testMigrationTo4_0_0(); testMigrationTo4_2_0(supportsAuthzServices); + testMigrationTo4_6_0(supportsAuthzServices, checkMigrationData); } protected void testMigrationTo4_x() { - testMigrationTo4_x(true); + testMigrationTo4_x(true, true); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java index 5a219e222e..c8dabda1f3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java @@ -71,7 +71,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo2_5_0(); //testMigrationTo2_5_1(); // Offline tokens migration is skipped for JSON testMigrationTo3_x(); - testMigrationTo4_x(false); + testMigrationTo4_x(false, false); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java index 0b79e00253..61afd71b45 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java @@ -65,7 +65,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat @Test public void migration2_5_5Test() throws Exception { testMigrationTo3_x(); - testMigrationTo4_x(); + testMigrationTo4_x(true, false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java index ae25bce736..835f1f562d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java @@ -63,7 +63,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat @Test public void migration3_4_3Test() throws Exception { - testMigrationTo4_x(); + testMigrationTo4_x(true, false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index da82d6e3c3..1a35d53f75 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -88,7 +88,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigratedData(false); testMigrationTo2_x(); testMigrationTo3_x(); - testMigrationTo4_x(false); + testMigrationTo4_x(false, false); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-3.4.3.Final.json b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-3.4.3.Final.json index 0f72b38c1f..fd39026ebc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-3.4.3.Final.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-3.4.3.Final.json @@ -858,7 +858,33 @@ } ], "useTemplateConfig" : false, "useTemplateScope" : false, - "useTemplateMappers" : false + "useTemplateMappers" : false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "resources": [ + { + "name": "group.resource.a", + "scopes": ["view-members"] + }, + { + "name": "group.resource.b", + "scopes": ["view-members"] + }, + { + "name": "group.resource.c", + "scopes": ["view-members"] + }, + { + "name": "group.resource.d", + "scopes": ["view-members"] + }, + { + "name": "group.resource.e", + "scopes": ["view-members"] + } + ] + } }, { "id" : "35d39054-e7e5-4dbb-8948-1738094679e8", "clientId" : "security-admin-console", diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 0cf03aacb8..54004de3a4 100755 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -235,12 +235,8 @@ public abstract class AbstractIdentityProviderTest { UserSessionStatus userSessionStatus = retrieveSessionStatus(); IDToken idToken = userSessionStatus.getIdToken(); KeycloakSession samlServerSession = brokerServerRule.startSession(); - try { - RealmModel brokerRealm = samlServerSession.realms().getRealm("realm-with-broker"); - return samlServerSession.users().getUserById(idToken.getSubject(), brokerRealm); - } finally { - brokerServerRule.stopSession(samlServerSession, false); - } + RealmModel brokerRealm = samlServerSession.realms().getRealm("realm-with-broker"); + return samlServerSession.users().getUserById(idToken.getSubject(), brokerRealm); } protected void doAfterProviderAuthentication() {