Calling getTopLevelGroups is slow inside GroupLDAPStorageMapper#getLDAPGroupMappingsConverted (#8430)

Closes #14820 
---------
Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
Bernd Bohmann 2023-09-20 17:20:43 +02:00 committed by GitHub
parent f8a9e0134a
commit bb2f59df87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 471 additions and 42 deletions

View file

@ -54,7 +54,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -338,7 +337,9 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
private void dropNonExistingKcGroups(RealmModel realm, SynchronizationResult syncResult, Set<String> visitedGroupIds) { private void dropNonExistingKcGroups(RealmModel realm, SynchronizationResult syncResult, Set<String> visitedGroupIds) {
// Remove keycloak groups, which don't exist in LDAP // Remove keycloak groups, which don't exist in LDAP
getAllKcGroups(realm) GroupModel parent = getKcGroupsPathGroup(realm);
getAllKcGroups(realm, parent)
.filter(kcGroup -> !visitedGroupIds.contains(kcGroup.getId())) .filter(kcGroup -> !visitedGroupIds.contains(kcGroup.getId()))
.forEach(kcGroup -> { .forEach(kcGroup -> {
logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName()); logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName());
@ -361,22 +362,22 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
} }
protected GroupModel findKcGroupByLDAPGroup(RealmModel realm, LDAPObject ldapGroup) { protected GroupModel findKcGroupByLDAPGroup(RealmModel realm, GroupModel parent, LDAPObject ldapGroup) {
String groupNameAttr = config.getGroupNameLdapAttribute(); String groupNameAttr = config.getGroupNameLdapAttribute();
String groupName = ldapGroup.getAttributeAsString(groupNameAttr); String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
if (config.isPreserveGroupsInheritance()) { if (config.isPreserveGroupsInheritance()) {
// Override if better effectivity or different algorithm is needed // Override if better effectivity or different algorithm is needed
return getAllKcGroups(realm) return getAllKcGroups(realm, parent)
.filter(group -> Objects.equals(group.getName(), groupName)).findFirst().orElse(null); .filter(group -> Objects.equals(group.getName(), groupName)).findFirst().orElse(null);
} else { } else {
// Without preserved inheritance, it's always at groups path // Without preserved inheritance, it's always at groups path
return KeycloakModelUtils.findGroupByPath(realm, getKcGroupPathFromLDAPGroupName(groupName)); return session.groups().getGroupByName(realm, parent, groupName);
} }
} }
protected GroupModel findKcGroupOrSyncFromLDAP(RealmModel realm, LDAPObject ldapGroup, UserModel user) { protected GroupModel findKcGroupOrSyncFromLDAP(RealmModel realm, GroupModel parent, LDAPObject ldapGroup, UserModel user) {
GroupModel kcGroup = findKcGroupByLDAPGroup(realm, ldapGroup); GroupModel kcGroup = findKcGroupByLDAPGroup(realm, parent, ldapGroup);
if (kcGroup == null) { if (kcGroup == null) {
@ -385,7 +386,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
// Better to sync all groups from LDAP with preserved inheritance // Better to sync all groups from LDAP with preserved inheritance
if (!syncFromLDAPPerformedInThisTransaction) { if (!syncFromLDAPPerformedInThisTransaction) {
syncDataFromFederationProviderToKeycloak(realm); syncDataFromFederationProviderToKeycloak(realm);
kcGroup = findKcGroupByLDAPGroup(realm, ldapGroup); kcGroup = findKcGroupByLDAPGroup(realm, parent, ldapGroup);
} }
} else { } else {
String groupNameAttr = config.getGroupNameLdapAttribute(); String groupNameAttr = config.getGroupNameLdapAttribute();
@ -656,11 +657,12 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) { if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser); List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
if (!ldapGroups.isEmpty()) {
GroupModel parent = getKcGroupsPathGroup(realm);
// Import role mappings from LDAP into Keycloak DB // Import role mappings from LDAP into Keycloak DB
for (LDAPObject ldapGroup : ldapGroups) { for (LDAPObject ldapGroup : ldapGroups) {
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(realm, ldapGroup, user); GroupModel kcGroup = findKcGroupOrSyncFromLDAP(realm, parent, ldapGroup, user);
if (kcGroup != null) { if (kcGroup != null) {
logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName()); logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName());
user.joinGroup(kcGroup); user.joinGroup(kcGroup);
@ -668,6 +670,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
} }
} }
} }
}
protected String getMembershipUserLdapAttribute() { protected String getMembershipUserLdapAttribute() {
@ -760,14 +763,18 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
} }
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser); List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
if (!ldapGroups.isEmpty()) {
GroupModel parent = getKcGroupsPathGroup(realm);
cachedLDAPGroupMappings = ldapGroups.stream() cachedLDAPGroupMappings = ldapGroups.stream()
.map(ldapGroup -> findKcGroupOrSyncFromLDAP(realm, ldapGroup, this)) .map(ldapGroup -> findKcGroupOrSyncFromLDAP(realm, parent, ldapGroup, this))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return cachedLDAPGroupMappings.stream(); return cachedLDAPGroupMappings.stream();
} }
return Stream.empty();
}
} }
// LDAP groups path operations // LDAP groups path operations
@ -783,7 +790,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
* Provides KC group defined as groups path or null (top-level group) if corresponding group is not available. * Provides KC group defined as groups path or null (top-level group) if corresponding group is not available.
*/ */
protected GroupModel getKcGroupsPathGroup(RealmModel realm) { protected GroupModel getKcGroupsPathGroup(RealmModel realm) {
return config.isTopLevelGroupsPath() ? null : KeycloakModelUtils.findGroupByPath(realm, config.getGroupsPath()); return config.isTopLevelGroupsPath() ? null : KeycloakModelUtils.findGroupByPath(session.groups(), realm, config.getGroupsPath());
} }
/** /**
@ -813,9 +820,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
/** /**
* Provides a stream of all KC groups (with their sub groups) from groups path configured by the "Groups Path" configuration property. * Provides a stream of all KC groups (with their sub groups) from groups path configured by the "Groups Path" configuration property.
*/ */
protected Stream<GroupModel> getAllKcGroups(RealmModel realm) { protected Stream<GroupModel> getAllKcGroups(RealmModel realm, GroupModel topParentGroup) {
GroupModel topParentGroup = getKcGroupsPathGroup(realm);
Stream<GroupModel> allGroups = realm.getGroupsStream(); Stream<GroupModel> allGroups = realm.getGroupsStream();
if (topParentGroup == null) return allGroups; if (topParentGroup == null) return allGroups;

View file

@ -304,7 +304,7 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(GroupMapperConfig.GROUPS_LDAP_FILTER)); LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(GroupMapperConfig.GROUPS_LDAP_FILTER));
String group = new GroupMapperConfig(config).getGroupsPath(); String group = new GroupMapperConfig(config).getGroupsPath();
if (!GroupMapperConfig.DEFAULT_LDAP_GROUPS_PATH.equals(group) && KeycloakModelUtils.findGroupByPath(realm, group) == null) { if (!GroupMapperConfig.DEFAULT_LDAP_GROUPS_PATH.equals(group) && KeycloakModelUtils.findGroupByPath(session.groups(), realm, group) == null) {
throw new ComponentValidationException("ldapErrorMissingGroupsPathGroup"); throw new ComponentValidationException("ldapErrorMissingGroupsPathGroup");
} }
} }

View file

@ -26,6 +26,7 @@ import org.keycloak.models.cache.infinispan.events.RealmCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.GroupListPredicate; import org.keycloak.models.cache.infinispan.stream.GroupListPredicate;
import org.keycloak.models.cache.infinispan.stream.HasRolePredicate; import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
import org.keycloak.models.cache.infinispan.stream.InClientPredicate; import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
import org.keycloak.models.cache.infinispan.stream.InGroupPredicate;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate; import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import java.util.Set; import java.util.Set;
@ -96,6 +97,10 @@ public class RealmCacheManager extends CacheManager {
addInvalidations(GroupListPredicate.create().realm(realmId), invalidations); addInvalidations(GroupListPredicate.create().realm(realmId), invalidations);
} }
public void groupNameInvalidations(String groupId, Set<String> invalidations) {
addInvalidations(InGroupPredicate.create().group(groupId), invalidations);
}
public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) { public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId)); invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
} }

View file

@ -277,6 +277,7 @@ public class RealmCacheSession implements CacheRealmProvider {
private void invalidateGroup(String id, String realmId, boolean invalidateQueries) { private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id); invalidateGroup(id);
cache.groupNameInvalidations(id, invalidations);
if (invalidateQueries) { if (invalidateQueries) {
cache.groupQueriesInvalidations(realmId, invalidations); cache.groupQueriesInvalidations(realmId, invalidations);
} }
@ -564,6 +565,14 @@ public class RealmCacheSession implements CacheRealmProvider {
return realm + ".top.groups"; return realm + ".top.groups";
} }
static String getGroupByNameCacheKey(String realm, String parentId, String name) {
if (parentId != null) {
return realm + ".group." + parentId + "." + name;
} else {
return realm + ".group.top." + name;
}
}
static String getRolesCacheKey(String container) { static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX; return container + ROLES_QUERY_SUFFIX;
} }
@ -886,6 +895,33 @@ public class RealmCacheSession implements CacheRealmProvider {
return adapter; return adapter;
} }
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
String cacheKey = getGroupByNameCacheKey(realm.getId(), parent != null? parent.getId(): null, name);
GroupNameQuery query = cache.get(cacheKey, GroupNameQuery.class);
if (query != null) {
logger.tracev("Group by name cache hit: {0}", name);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
GroupModel model = getGroupDelegate().getGroupByName(realm, parent, name);
if (model == null) return null;
if (invalidations.contains(model.getId())) return model;
query = new GroupNameQuery(loaded, cacheKey, model.getId(), realm);
cache.addRevisioned(query, startupRevision);
return model;
} else if (invalidations.contains(cacheKey)) {
return getGroupDelegate().getGroupByName(realm, parent, name);
} else {
String groupId = query.getGroupId();
if (invalidations.contains(groupId)) {
return getGroupDelegate().getGroupByName(realm, parent, name);
}
return getGroupById(realm, groupId);
}
}
@Override @Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) { public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
invalidateGroup(group.getId(), realm.getId(), true); invalidateGroup(group.getId(), realm.getId(), true);

View file

@ -0,0 +1,49 @@
/*
* Copyright 2022 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.entities;
import org.keycloak.models.RealmModel;
public class GroupNameQuery extends AbstractRevisioned implements InRealm {
private final String realm;
private final String realmName;
private final String groupId;
public GroupNameQuery(Long revisioned, String id, String groupId, RealmModel realm) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.groupId = groupId;
}
public String getGroupId() {
return groupId;
}
public String getRealm() {
return realm;
}
@Override
public String toString() {
return "GroupNameQuery{" +
"id='" + getId() + "'" +
"realmName='" + realmName + '\'' +
'}';
}
}

View file

@ -62,6 +62,7 @@ public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInva
@Override @Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) { public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations); realmCache.groupQueriesInvalidations(realmId, invalidations);
realmCache.groupNameInvalidations(groupId, invalidations);
if (newParentId != null) { if (newParentId != null) {
invalidations.add(newParentId); invalidations.add(newParentId);
} }

View file

@ -60,6 +60,7 @@ public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheIn
@Override @Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) { public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations); realmCache.groupQueriesInvalidations(realmId, invalidations);
realmCache.groupNameInvalidations(groupId, invalidations);
if (parentId != null) { if (parentId != null) {
invalidations.add(parentId); invalidations.add(parentId);
} }

View file

@ -54,7 +54,7 @@ public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheIn
@Override @Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) { public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID already invalidated realmCache.groupNameInvalidations(groupId, invalidations);
} }
public static class ExternalizerImpl implements Externalizer<GroupUpdatedEvent> { public static class ExternalizerImpl implements Externalizer<GroupUpdatedEvent> {

View file

@ -0,0 +1,83 @@
/*
* Copyright 2022 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.stream;
import org.keycloak.models.cache.infinispan.entities.GroupNameQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
@SerializeWith(InGroupPredicate.ExternalizerImpl.class)
public class InGroupPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String group;
public static InGroupPredicate create() {
return new InGroupPredicate();
}
public InGroupPredicate group(String id) {
group = id;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof GroupNameQuery)) return false;
return group.equals(((GroupNameQuery)value).getGroupId());
}
public static class ExternalizerImpl implements Externalizer<InGroupPredicate> {
private static final int VERSION_1 = 1;
@Override
public void writeObject(ObjectOutput output, InGroupPredicate obj) throws IOException {
output.writeByte(VERSION_1);
MarshallUtil.marshallString(obj.group, output);
}
@Override
public InGroupPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException {
switch (input.readByte()) {
case VERSION_1:
return readObjectVersion1(input);
default:
throw new IOException("Unknown version");
}
}
public InGroupPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
InGroupPredicate res = new InGroupPredicate();
res.group = MarshallUtil.unmarshallString(input);
return res;
}
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2022 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.sessions.infinispan.entities.wildfly;
import org.keycloak.models.cache.infinispan.stream.InGroupPredicate;
public class InGroupPredicateWFExternalizer extends InfinispanExternalizerAdapter<InGroupPredicate> {
public InGroupPredicateWFExternalizer() {
super(InGroupPredicate.class, new InGroupPredicate.ExternalizerImpl());
}
}

View file

@ -55,6 +55,7 @@ org.keycloak.models.sessions.infinispan.entities.wildfly.LockEntryWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.ActionTokenValueEntityWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.ActionTokenValueEntityWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.RoleAddedEventWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.RoleAddedEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.InClientPredicateWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.InClientPredicateWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.InGroupPredicateWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.UserFullInvalidationEventWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.UserFullInvalidationEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.ClientRemovedSessionEventWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.ClientRemovedSessionEventWFExternalizer
org.keycloak.models.sessions.infinispan.entities.wildfly.SessionPredicateWFExternalizer org.keycloak.models.sessions.infinispan.entities.wildfly.SessionPredicateWFExternalizer

View file

@ -435,6 +435,20 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
return adapter; return adapter;
} }
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdByNameAndParent", String.class);
query.setParameter("name", name);
query.setParameter("realm", realm.getId());
query.setParameter("parent", parent != null ? parent.getId() : GroupEntity.TOP_PARENT_ID);
List<String> entities = query.getResultList();
if (entities.isEmpty()) return null;
if (entities.size() > 1) throw new IllegalStateException("Should not be more than one Group with same name");
String id = query.getResultList().get(0);
return session.groups().getGroupById(realm, id);
}
@Override @Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) { public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
if (toParent != null && group.getId().equals(toParent.getId())) { if (toParent != null && group.getId().equals(toParent.getId())) {

View file

@ -37,7 +37,8 @@ import java.util.LinkedList;
@NamedQuery(name="getGroupCountByNameContainingFromIdList", query="select count(u) from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids"), @NamedQuery(name="getGroupCountByNameContainingFromIdList", query="select count(u) from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids"),
@NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parentId = :parent and u.realm = :realm order by u.name ASC"), @NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parentId = :parent and u.realm = :realm order by u.name ASC"),
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"), @NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"),
@NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm and u.parentId = :parent") @NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm and u.parentId = :parent"),
@NamedQuery(name="getGroupIdByNameAndParent", query="select u.id from GroupEntity u where u.realm = :realm and u.parentId = :parent and u.name = :name")
}) })
@Entity @Entity
@Table(name="KEYCLOAK_GROUP", @Table(name="KEYCLOAK_GROUP",

View file

@ -55,6 +55,11 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
return provider.getGroupById(realm, id); return provider.getGroupById(realm, id);
} }
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
return localStorage().getGroupByName(realm, parent, name);
}
@Override @Override
public Stream<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) { public Stream<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = localStorage().searchGroupsByAttributes(realm, attributes, firstResult, maxResults); Stream<GroupModel> local = localStorage().searchGroupsByAttributes(realm, attributes, firstResult, maxResults);

View file

@ -93,6 +93,28 @@ public class MapGroupProvider implements GroupProvider {
: entityToAdapterFunc(realm).apply(entity); : entityToAdapterFunc(realm).apply(entity);
} }
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
if (name == null) {
return null;
}
LOG.tracef("getGroupByName(%s, %s)%s", realm, name, getShortStackTrace());
DefaultModelCriteria<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.NAME, Operator.EQ, name)
.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
if (parent != null) {
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, parent.getId());
} else {
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS);
}
QueryParameters<GroupModel> queryParameters = withCriteria(mcb);
String groupId = storeWithRealm(realm).read(queryParameters).findFirst().map(MapGroupEntity::getId)
.orElse(null);
return groupId == null ? null : session.groups().getGroupById(realm, groupId);
}
@Override @Override
public Stream<GroupModel> getGroupsStream(RealmModel realm) { public Stream<GroupModel> getGroupsStream(RealmModel realm) {
return getGroupsStreamInternal(realm, null, null); return getGroupsStreamInternal(realm, null, null);

View file

@ -36,6 +36,7 @@ import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSecretConstants; import org.keycloak.models.ClientSecretConstants;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -763,6 +764,43 @@ public final class KeycloakModelUtils {
return segments; return segments;
} }
public static GroupModel findGroupByPath(GroupProvider groupProvider, RealmModel realm, String path) {
if (path == null) {
return null;
}
if (path.startsWith(GROUP_PATH_SEPARATOR)) {
path = path.substring(1);
}
if (path.endsWith(GROUP_PATH_SEPARATOR)) {
path = path.substring(0, path.length() - 1);
}
String[] split = path.split(GROUP_PATH_SEPARATOR);
if (split.length == 0) return null;
return getGroupModel(groupProvider, realm, null, split, 0);
}
private static GroupModel getGroupModel(GroupProvider groupProvider, RealmModel realm, GroupModel parent, String[] split, int index) {
StringBuilder nameBuilder = new StringBuilder();
for (int i = index; i < split.length; i++) {
nameBuilder.append(split[i]);
GroupModel group = groupProvider.getGroupByName(realm, parent, nameBuilder.toString());
if (group != null) {
if (i < split.length-1) {
return getGroupModel(groupProvider, realm, group, split, i+1);
} else {
return group;
}
}
nameBuilder.append(GROUP_PATH_SEPARATOR);
}
return null;
}
/**
*
* @deprecated please use {@link #findGroupByPath(GroupProvider, RealmModel, String)} instead
*/
@Deprecated
public static GroupModel findGroupByPath(RealmModel realm, String path) { public static GroupModel findGroupByPath(RealmModel realm, String path) {
if (path == null) { if (path == null) {
return null; return null;

View file

@ -19,9 +19,7 @@ package org.keycloak.storage.group;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public interface GroupLookupProvider { public interface GroupLookupProvider {
@ -35,6 +33,19 @@ public interface GroupLookupProvider {
*/ */
GroupModel getGroupById(RealmModel realm, String id); GroupModel getGroupById(RealmModel realm, String id);
/**
* Returns a group from the given realm with the corresponding name and parent
*
* @param realm Realm.
* @param parent parent Group. If {@code null} top level groups are searched
* @param name name.
* @return GroupModel with the corresponding name.
*/
default GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
return (parent == null ? realm.getTopLevelGroupsStream() : parent.getSubGroupsStream())
.filter(groupModel -> groupModel.getName().equals(name)).findFirst().orElse(null);
}
/** /**
* Returns the group hierarchy with the given string in name for the given realm. * Returns the group hierarchy with the given string in name for the given realm.
* *

View file

@ -52,6 +52,12 @@ public class HardcodedGroupStorageProvider implements GroupStorageProvider {
return null; return null;
} }
@Override
public GroupModel getGroupByName(RealmModel realm, GroupModel parent, String name) {
if (this.groupName.equals(name)) return new HardcodedGroupAdapter(realm);
return null;
}
@Override @Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) {
if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH))) try { if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH))) try {

View file

@ -94,31 +94,31 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
// 1 - Grant some groups in LDAP // 1 - Grant some groups in LDAP
// This group should already exists as it was imported from LDAP // This group should already exists as it was imported from LDAP
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); GroupModel group1 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1");
john.joinGroup(group1); john.joinGroup(group1);
// This group should already exists as it was imported from LDAP // This group should already exists as it was imported from LDAP
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); GroupModel group11 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1/group11");
mary.joinGroup(group11); mary.joinGroup(group11);
// This group should already exists as it was imported from LDAP // This group should already exists as it was imported from LDAP
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); GroupModel group12 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1/group12");
john.joinGroup(group12); john.joinGroup(group12);
mary.joinGroup(group12); mary.joinGroup(group12);
// This group should already exists as it was imported from LDAP // This group should already exists as it was imported from LDAP
GroupModel groupWithSlashesInName = KeycloakModelUtils.findGroupByPath(appRealm, "Team 2016/2017"); GroupModel groupWithSlashesInName = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "Team 2016/2017");
john.joinGroup(groupWithSlashesInName); john.joinGroup(groupWithSlashesInName);
mary.joinGroup(groupWithSlashesInName); mary.joinGroup(groupWithSlashesInName);
// This group should already exists as it was imported from LDAP // This group should already exists as it was imported from LDAP
GroupModel groupChildWithSlashesInName = KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019"); GroupModel groupChildWithSlashesInName = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "defaultGroup1/Team Child 2018/2019");
john.joinGroup(groupChildWithSlashesInName); john.joinGroup(groupChildWithSlashesInName);
mary.joinGroup(groupChildWithSlashesInName); mary.joinGroup(groupChildWithSlashesInName);
Assert.assertEquals("Team SubChild 2020/2021", KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019/Team SubChild 2020/2021").getName()); Assert.assertEquals("Team SubChild 2020/2021", KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "defaultGroup1/Team Child 2018/2019/Team SubChild 2020/2021").getName());
Assert.assertEquals("defaultGroup14", KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup13/Team SubChild 2022/2023/A/B/C/D/E/defaultGroup14").getName()); Assert.assertEquals("defaultGroup14", KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "defaultGroup13/Team SubChild 2022/2023/A/B/C/D/E/defaultGroup14").getName());
Assert.assertEquals("Team SubChild 2026/2027", KeycloakModelUtils.findGroupByPath(appRealm, "Team Root 2024/2025/A/B/C/D/defaultGroup15/Team SubChild 2026/2027").getName()); Assert.assertEquals("Team SubChild 2026/2027", KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "Team Root 2024/2025/A/B/C/D/defaultGroup15/Team SubChild 2026/2027").getName());
}); });
@ -146,11 +146,11 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
LDAPTestContext ctx = LDAPTestContext.init(session); LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm(); RealmModel appRealm = ctx.getRealm();
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); GroupModel group1 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1");
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); GroupModel group11 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1/group11");
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); GroupModel group12 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "/group1/group12");
GroupModel groupTeam20162017 = KeycloakModelUtils.findGroupByPath(appRealm, "Team 2016/2017"); GroupModel groupTeam20162017 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "Team 2016/2017");
GroupModel groupTeamChild20182019 = KeycloakModelUtils.findGroupByPath(appRealm, "defaultGroup1/Team Child 2018/2019"); GroupModel groupTeamChild20182019 = KeycloakModelUtils.findGroupByPath(session.groups(), appRealm, "defaultGroup1/Team Child 2018/2019");
UserModel john = session.users().getUserByUsername(appRealm, "johnkeycloak"); UserModel john = session.users().getUserByUsername(appRealm, "johnkeycloak");
UserModel mary = session.users().getUserByUsername(appRealm, "marykeycloak"); UserModel mary = session.users().getUserByUsername(appRealm, "marykeycloak");

View file

@ -22,6 +22,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.testsuite.model.KeycloakModelTest; import org.keycloak.testsuite.model.KeycloakModelTest;
import java.util.Arrays; import java.util.Arrays;
@ -29,9 +30,11 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
public class GroupModelTest extends KeycloakModelTest { public class GroupModelTest extends KeycloakModelTest {
@ -99,4 +102,125 @@ public class GroupModelTest extends KeycloakModelTest {
}); });
} }
@Test
public void testGroupByName() {
String subGroupId1 = withRealm(realmId, (session, realm) -> {
GroupModel group = session.groups().createGroup(realm, "parent-1");
GroupModel subGroup = session.groups().createGroup(realm, "sub-group-1", group);
return subGroup.getId();
});
String subGroupId2 = withRealm(realmId, (session, realm) -> {
GroupModel group = session.groups().createGroup(realm, "parent-2");
GroupModel subGroup = session.groups().createGroup(realm, "sub-group-1", group);
return subGroup.getId();
});
withRealm(realmId, (session, realm) -> {
GroupModel group1 = session.groups().getGroupByName(realm, null,"parent-1");
GroupModel group2 = session.groups().getGroupByName(realm, null,"parent-2");
GroupModel subGroup1 = session.groups().getGroupByName(realm, group1,"sub-group-1");
GroupModel subGroup2 = session.groups().getGroupByName(realm, group2,"sub-group-1");
assertThat(subGroup1.getId(), equalTo(subGroupId1));
assertThat(subGroup1.getName(), equalTo("sub-group-1"));
assertThat(subGroup2.getId(), equalTo(subGroupId2));
assertThat(subGroup2.getName(), equalTo("sub-group-1"));
return null;
});
}
@Test
public void testConflictingNames() {
final String conflictingGroupName = "conflicting-group-name";
String parentGroupWithChildId = withRealm(realmId, (session, realm) -> {
GroupModel parentGroupWithChild = session.groups().createGroup(realm, "parent-1");
GroupModel subGroup1 = session.groups().createGroup(realm, conflictingGroupName, parentGroupWithChild);
return parentGroupWithChild.getId();
});
String parentGroupWithConflictingNameId = withRealm(realmId, (session, realm) -> session.groups().createGroup(realm, conflictingGroupName).getId());
String parentGroupWithoutChildrenId = withRealm(realmId, (session, realm) -> session.groups().createGroup(realm, "parent-2").getId());
withRealm(realmId, (session, realm) -> {
GroupModel searchedGroup = session.groups().getGroupByName(realm, null, conflictingGroupName);
assertThat(searchedGroup, notNullValue());
assertThat(searchedGroup.getId(), equalTo(parentGroupWithConflictingNameId));
return null;
});
withRealm(realmId, (session, realm) -> {
GroupModel parentGroupWithChild = session.groups().getGroupById(realm, parentGroupWithChildId);
GroupModel searchedGroup = session.groups().getGroupByName(realm, parentGroupWithChild, conflictingGroupName);
assertThat(searchedGroup, notNullValue());
assertThat(searchedGroup.getParentId(), equalTo(parentGroupWithChildId));
return null;
});
withRealm(realmId, (session, realm) -> {
GroupModel parentGroupWithoutChildren = session.groups().getGroupById(realm, parentGroupWithoutChildrenId);
GroupModel searchedGroup = session.groups().getGroupByName(realm, parentGroupWithoutChildren, conflictingGroupName);
assertThat(searchedGroup, nullValue());
return null;
});
}
@Test
public void testGroupByNameCacheInvalidation() {
String subGroupId1 = withRealm(realmId, (session, realm) -> {
GroupModel group = session.groups().createGroup(realm, "parent-1");
GroupModel subGroup = session.groups().createGroup(realm, "sub-group-1", group);
return subGroup.getId();
});
withRealm(realmId, (session, realm) -> {
GroupModel group1 = session.groups().getGroupByName(realm, null, "parent-1");
GroupModel subGroup1 = session.groups().getGroupByName(realm, group1, "sub-group-1");
assertThat(subGroup1.getId(), equalTo(subGroupId1));
return null;
});
withRealm(realmId, (session, realm) -> {
GroupModel group1 = session.groups().getGroupByName(realm, null, "parent-1");
GroupModel subGroup1 = session.groups().getGroupByName(realm, group1, "sub-group-1");
session.groups().removeGroup(realm, subGroup1);
return null;
});
withRealm(realmId, (session, realm) -> {
GroupModel group1 = session.groups().getGroupByName(realm, null, "parent-1");
GroupModel subGroup1 = session.groups().getGroupByName(realm, group1, "sub-group-1");
assertThat(subGroup1, nullValue());
return null;
});
}
@Test
public void testFindGroupByPath() {
String subGroupId1 = withRealm(realmId, (session, realm) -> {
GroupModel group = session.groups().createGroup(realm, "parent-1");
GroupModel subGroup = session.groups().createGroup(realm, "sub-group-1", group);
return subGroup.getId();
});
String subGroupIdWithSlash = withRealm(realmId, (session, realm) -> {
GroupModel group = session.groups().createGroup(realm, "parent-2");
GroupModel subGroup = session.groups().createGroup(realm, "sub-group/1", group);
return subGroup.getId();
});
withRealm(realmId, (session, realm) -> {
GroupModel group1 = KeycloakModelUtils.findGroupByPath(session.groups(), realm, "/parent-1");
GroupModel group2 = KeycloakModelUtils.findGroupByPath(session.groups(), realm, "/parent-2");
assertThat(group1.getName(), equalTo("parent-1"));
assertThat(group2.getName(), equalTo("parent-2"));
GroupModel subGroup1 = KeycloakModelUtils.findGroupByPath(session.groups(), realm, "/parent-1/sub-group-1");
GroupModel subGroup2 = KeycloakModelUtils.findGroupByPath(session.groups(), realm, "/parent-2/sub-group/1");
assertThat(subGroup1.getId(), equalTo(subGroupId1));
assertThat(subGroup2.getId(), equalTo(subGroupIdWithSlash));
return null;
});
}
} }