KEYCLOAK-14974 Map group storage provider
This commit is contained in:
parent
2cd03569d6
commit
12bc84322a
33 changed files with 1149 additions and 163 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -71,4 +71,4 @@ jobs:
|
|||
- name: Build testsuite
|
||||
run: mvn clean install -B -DskipTests -f testsuite/pom.xml
|
||||
- name: Run base tests - undertow
|
||||
run: mvn clean install -B -f testsuite/integration-arquillian/tests/base/pom.xml -Dkeycloak.client.provider=map | misc/log/trimmer.sh; exit ${PIPESTATUS[0]}
|
||||
run: mvn clean install -B -f testsuite/integration-arquillian/tests/base/pom.xml -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map | misc/log/trimmer.sh; exit ${PIPESTATUS[0]}
|
|
@ -82,6 +82,10 @@ public interface UserResource {
|
|||
@QueryParam("max") Integer maxResults,
|
||||
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
|
||||
|
||||
@Path("groups/count")
|
||||
@GET
|
||||
Map<String, Long> groupsCount(@QueryParam("search") String search);
|
||||
|
||||
@Path("groups/{groupId}")
|
||||
@PUT
|
||||
void joinGroup(@PathParam("groupId") String groupId);
|
||||
|
|
|
@ -466,7 +466,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
static String getRealmByNameCacheKey(String name) {
|
||||
return "realm.query.by.name." + name;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<RealmModel> getRealmsWithProviderType(Class<?> type) {
|
||||
// Retrieve realms from backend
|
||||
|
@ -589,12 +589,12 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
|
||||
if (client.isServiceAccountsEnabled()) {
|
||||
UserModel serviceAccount = session.users().getServiceAccount(client);
|
||||
|
||||
|
||||
if (serviceAccount != null) {
|
||||
session.users().removeUser(realm, serviceAccount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return getClientDelegate().removeClient(realm, id);
|
||||
}
|
||||
|
||||
|
@ -687,7 +687,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
}
|
||||
return list.stream();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, Integer max) {
|
||||
return getRoleDelegate().getRealmRolesStream(realm, first, max);
|
||||
|
@ -697,7 +697,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
|
||||
return getRoleDelegate().getClientRolesStream(client, first, max);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) {
|
||||
return getRoleDelegate().searchForClientRolesStream(client, search, first, max);
|
||||
|
@ -899,7 +899,16 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
list.add(group);
|
||||
}
|
||||
|
||||
return list.stream().sorted(Comparator.comparing(GroupModel::getName));
|
||||
return list.stream().sorted(GroupModel.COMPARE_BY_NAME);
|
||||
}
|
||||
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
|
||||
return getGroupDelegate().getGroupsStream(realm, ids, search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Stream<String> ids, String search) {
|
||||
return getGroupDelegate().getGroupsCount(realm, ids, search);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -916,9 +925,9 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
|
||||
return getGroupDelegate().getGroupsCountByNameContaining(realm, search);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||
return getGroupDelegate().getGroupsByRoleStream(realm, role, firstResult, maxResults);
|
||||
}
|
||||
|
||||
|
@ -956,13 +965,14 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
list.add(group);
|
||||
}
|
||||
|
||||
return list.stream().sorted(Comparator.comparing(GroupModel::getName));
|
||||
return list.stream().sorted(GroupModel.COMPARE_BY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer first, Integer max) {
|
||||
String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max);
|
||||
boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max);
|
||||
boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max)
|
||||
|| listInvalidations.contains(realm.getId());
|
||||
if (queryDB) {
|
||||
return getGroupDelegate().getTopLevelGroupsStream(realm, first, max);
|
||||
}
|
||||
|
@ -993,7 +1003,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
list.add(group);
|
||||
}
|
||||
|
||||
return list.stream().sorted(Comparator.comparing(GroupModel::getName));
|
||||
return list.stream().sorted(GroupModel.COMPARE_BY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1043,6 +1053,11 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm, RoleModel role) {
|
||||
getGroupDelegate().preRemove(realm, role);
|
||||
}
|
||||
|
||||
private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
|
||||
String groupId = eventToAdd.getId();
|
||||
|
||||
|
|
|
@ -341,6 +341,12 @@ public class UserAdapter implements CachedUserModel {
|
|||
return groups.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupsCountByNameContaining(String search) {
|
||||
if (updated != null) return updated.getGroupsCountByNameContaining(search);
|
||||
return modelSupplier.get().getGroupsCountByNameContaining(search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void joinGroup(GroupModel group) {
|
||||
getDelegateForUpdate();
|
||||
|
|
|
@ -108,7 +108,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
RealmAdapter adapter = new RealmAdapter(session, em, realm);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<RealmModel> getRealmsWithProviderType(Class<?> providerType) {
|
||||
TypedQuery<String> query = em.createNamedQuery("getRealmIdsWithProviderType", String.class);
|
||||
|
@ -154,7 +154,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
final RealmAdapter adapter = new RealmAdapter(session, em, realm);
|
||||
session.users().preRemove(adapter);
|
||||
|
||||
realm.getDefaultGroups().clear();
|
||||
realm.getDefaultGroupIds().clear();
|
||||
em.flush();
|
||||
|
||||
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
|
||||
|
@ -171,8 +171,8 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
|
||||
removeRoles(adapter);
|
||||
|
||||
adapter.getGroupsStream().forEach(adapter::removeGroup);
|
||||
|
||||
adapter.getTopLevelGroupsStream().forEach(adapter::removeGroup);
|
||||
|
||||
num = em.createNamedQuery("removeClientInitialAccessByRealm")
|
||||
.setParameter("realm", realm).executeUpdate();
|
||||
|
||||
|
@ -272,12 +272,12 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
if (roles.isEmpty()) return null;
|
||||
return session.roles().getRoleById(client.getRealm(), roles.get(0));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, Integer max) {
|
||||
TypedQuery<RoleEntity> query = em.createNamedQuery("getRealmRoles", RoleEntity.class);
|
||||
query.setParameter("realm", realm.getId());
|
||||
|
||||
|
||||
return getRolesStream(query, realm, first, max);
|
||||
}
|
||||
|
||||
|
@ -299,22 +299,22 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
|
||||
return closing(results.map(role -> new RoleAdapter(session, realm, em, role)));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) {
|
||||
TypedQuery<RoleEntity> query = em.createNamedQuery("searchForClientRoles", RoleEntity.class);
|
||||
query.setParameter("client", client.getId());
|
||||
return searchForRoles(query, client.getRealm(), search, first, max);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) {
|
||||
TypedQuery<RoleEntity> query = em.createNamedQuery("searchForRealmRoles", RoleEntity.class);
|
||||
query.setParameter("realm", realm.getId());
|
||||
|
||||
|
||||
return searchForRoles(query, realm, search, first, max);
|
||||
}
|
||||
|
||||
|
||||
protected Stream<RoleModel> searchForRoles(TypedQuery<RoleEntity> query, RealmModel realm, String search, Integer first, Integer max) {
|
||||
|
||||
query.setParameter("search", "%" + search.trim().toLowerCase() + "%");
|
||||
|
@ -322,9 +322,9 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
&& first >= 0 && max >= 0) {
|
||||
query= query.setFirstResult(first).setMaxResults(max);
|
||||
}
|
||||
|
||||
|
||||
Stream<RoleEntity> results = query.getResultStream();
|
||||
|
||||
|
||||
return closing(results.map(role -> new RoleAdapter(session, realm, em, role)));
|
||||
}
|
||||
|
||||
|
@ -352,7 +352,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate();
|
||||
realm.getClientsStream().forEach(c -> c.deleteScopeMapping(role));
|
||||
em.createNamedQuery("deleteClientScopeRoleMappingByRole").setParameter("role", roleEntity).executeUpdate();
|
||||
int val = em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
|
||||
session.groups().preRemove(realm, role);
|
||||
|
||||
em.flush();
|
||||
em.remove(roleEntity);
|
||||
|
@ -415,15 +415,79 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
group.setParent(toParent);
|
||||
if (toParent != null) toParent.addChild(group);
|
||||
else session.groups().addTopLevelGroup(realm, group);
|
||||
|
||||
// TODO: Remove em.flush(), currently this needs to be there to translate ConstraintViolationException to
|
||||
// DuplicateModelException {@link PersistenceExceptionConverter} is not called if the
|
||||
// ConstraintViolationException is not thrown in method called directly from EntityManager
|
||||
em.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm) {
|
||||
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
|
||||
return closing(em.createNamedQuery("getGroupIdsByRealm", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.getResultStream())
|
||||
.map(g -> session.groups().getGroupById(realm, g));
|
||||
}
|
||||
|
||||
return ref.getGroups().stream()
|
||||
.map(g -> session.groups().getGroupById(realm, g.getId()))
|
||||
.sorted(Comparator.comparing(GroupModel::getName));
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
|
||||
if (search == null || search.isEmpty()) return getGroupsStream(realm, ids, first, max);
|
||||
|
||||
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContainingFromIdList", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setParameter("search", search)
|
||||
.setParameter("ids", ids.collect(Collectors.toList()));
|
||||
|
||||
return closing(paginateQuery(query, first, max).getResultStream())
|
||||
.map(g -> session.groups().getGroupById(realm, g));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, Integer first, Integer max) {
|
||||
if (first == null && max == null) {
|
||||
return getGroupsStream(realm, ids);
|
||||
}
|
||||
|
||||
TypedQuery<String> query = em.createNamedQuery("getGroupIdsFromIdList", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setParameter("ids", ids.collect(Collectors.toList()));
|
||||
|
||||
|
||||
return closing(paginateQuery(query, first, max).getResultStream())
|
||||
.map(g -> session.groups().getGroupById(realm, g));
|
||||
}
|
||||
|
||||
private static <T> TypedQuery<T> paginateQuery(TypedQuery<T> query, Integer first, Integer max) {
|
||||
if (first != null && first > 0) {
|
||||
query = query.setFirstResult(first);
|
||||
}
|
||||
|
||||
if (max != null && max >= 0) {
|
||||
query = query.setMaxResults(max);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids) {
|
||||
return ids.map(id -> session.groups().getGroupById(realm, id)).sorted(GroupModel.COMPARE_BY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Stream<String> ids, String search) {
|
||||
TypedQuery<Long> query;
|
||||
if (search != null && !search.isEmpty()) {
|
||||
query = em.createNamedQuery("getGroupCountByNameContainingFromIdList", Long.class)
|
||||
.setParameter("search", search);
|
||||
} else {
|
||||
query = em.createNamedQuery("getGroupIdsFromIdList", Long.class);
|
||||
}
|
||||
|
||||
return query.setParameter("realm", realm.getId())
|
||||
.setParameter("ids", ids.collect(Collectors.toList()))
|
||||
.getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -452,44 +516,39 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
|
||||
return searchForGroupByNameStream(realm, search, null, null).count();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||
TypedQuery<GroupEntity> query = em.createNamedQuery("groupsInRole", GroupEntity.class);
|
||||
query.setParameter("roleId", role.getId());
|
||||
if (firstResult != -1) {
|
||||
query.setFirstResult(firstResult);
|
||||
if (firstResult != null && firstResult > 0) {
|
||||
query = query.setFirstResult(firstResult);
|
||||
}
|
||||
if (maxResults != -1) {
|
||||
query.setMaxResults(maxResults);
|
||||
if (maxResults != null && maxResults > 0) {
|
||||
query = query.setMaxResults(maxResults);
|
||||
}
|
||||
Stream<GroupEntity> results = query.getResultStream();
|
||||
|
||||
return closing(results
|
||||
.map(g -> (GroupModel) new GroupAdapter(realm, em, g))
|
||||
.sorted(Comparator.comparing(GroupModel::getName)));
|
||||
.sorted(GroupModel.COMPARE_BY_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm) {
|
||||
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
|
||||
|
||||
return ref.getGroups().stream()
|
||||
.filter(g -> GroupEntity.TOP_PARENT_ID.equals(g.getParentId()))
|
||||
.map(g -> session.groups().getGroupById(realm, g.getId()))
|
||||
.sorted(Comparator.comparing(GroupModel::getName));
|
||||
return getTopLevelGroupsStream(realm, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer first, Integer max) {
|
||||
Stream<String> groupIds = em.createNamedQuery("getTopLevelGroupIds", String.class)
|
||||
TypedQuery<String> groupsQuery = em.createNamedQuery("getTopLevelGroupIds", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setParameter("parent", GroupEntity.TOP_PARENT_ID)
|
||||
.setFirstResult(first)
|
||||
.setMaxResults(max)
|
||||
.getResultStream();
|
||||
.setParameter("parent", GroupEntity.TOP_PARENT_ID);
|
||||
|
||||
return closing(groupIds.map(realm::getGroupById));
|
||||
return closing(paginateQuery(groupsQuery, first, max).getResultStream()
|
||||
.map(realm::getGroupById)
|
||||
.sorted(GroupModel.COMPARE_BY_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -527,9 +586,6 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
}
|
||||
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
|
||||
|
||||
RealmEntity realmEntity = em.getReference(RealmEntity.class, realm.getId());
|
||||
realmEntity.getGroups().remove(groupEntity);
|
||||
|
||||
em.remove(groupEntity);
|
||||
return true;
|
||||
|
||||
|
@ -547,15 +603,12 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
GroupEntity groupEntity = new GroupEntity();
|
||||
groupEntity.setId(id);
|
||||
groupEntity.setName(name);
|
||||
RealmEntity realmEntity = em.getReference(RealmEntity.class, realm.getId());
|
||||
groupEntity.setRealm(realmEntity.getId());
|
||||
groupEntity.setRealm(realm.getId());
|
||||
groupEntity.setParentId(toParent == null? GroupEntity.TOP_PARENT_ID : toParent.getId());
|
||||
em.persist(groupEntity);
|
||||
em.flush();
|
||||
realmEntity.getGroups().add(groupEntity);
|
||||
|
||||
GroupAdapter adapter = new GroupAdapter(realm, em, groupEntity);
|
||||
return adapter;
|
||||
return new GroupAdapter(realm, em, groupEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -563,6 +616,13 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
subGroup.setParent(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm, RoleModel role) {
|
||||
// GroupProvider method implementation starts here
|
||||
em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", role.getId()).executeUpdate();
|
||||
// GroupProvider method implementation ends here
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel addClient(RealmModel realm, String clientId) {
|
||||
return addClient(realm, KeycloakModelUtils.generateId(), clientId);
|
||||
|
@ -733,10 +793,8 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setParameter("search", search);
|
||||
if(Objects.nonNull(first) && Objects.nonNull(max)) {
|
||||
query= query.setFirstResult(first).setMaxResults(max);
|
||||
}
|
||||
Stream<String> groups = query.getResultStream();
|
||||
|
||||
Stream<String> groups = paginateQuery(query, first, max).getResultStream();
|
||||
|
||||
return closing(groups.map(id -> {
|
||||
GroupModel groupById = session.groups().getGroupById(realm, id);
|
||||
|
@ -744,7 +802,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
|
|||
groupById = session.groups().getGroupById(realm, groupById.getParentId());
|
||||
}
|
||||
return groupById;
|
||||
}).sorted(Comparator.comparing(GroupModel::getName)).distinct());
|
||||
}).sorted(GroupModel.COMPARE_BY_NAME).distinct());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -789,38 +789,27 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
@Override
|
||||
public Stream<GroupModel> getDefaultGroupsStream() {
|
||||
Collection<GroupEntity> entities = realm.getDefaultGroups();
|
||||
if (entities == null || entities.isEmpty()) return Stream.empty();
|
||||
return entities.stream().map(GroupEntity::getId).map(this::getGroupById);
|
||||
return realm.getDefaultGroupIds().stream().map(this::getGroupById);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDefaultGroup(GroupModel group) {
|
||||
Collection<GroupEntity> entities = realm.getDefaultGroups();
|
||||
for (GroupEntity entity : entities) {
|
||||
if (entity.getId().equals(group.getId())) return;
|
||||
}
|
||||
GroupEntity groupEntity = GroupAdapter.toEntity(group, em);
|
||||
realm.getDefaultGroups().add(groupEntity);
|
||||
em.flush();
|
||||
Collection<String> groupsIds = realm.getDefaultGroupIds();
|
||||
if (groupsIds.contains(group.getId())) return;
|
||||
|
||||
groupsIds.add(group.getId());
|
||||
em.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeDefaultGroup(GroupModel group) {
|
||||
GroupEntity found = null;
|
||||
for (GroupEntity defaultGroup : realm.getDefaultGroups()) {
|
||||
if (defaultGroup.getId().equals(group.getId())) {
|
||||
found = defaultGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
realm.getDefaultGroups().remove(found);
|
||||
Collection<String> groupIds = realm.getDefaultGroupIds();
|
||||
|
||||
if (groupIds.remove(group.getId())) {
|
||||
em.flush();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ClientModel> getClientsStream() {
|
||||
return session.clients().getClientsStream(this);
|
||||
|
|
|
@ -43,12 +43,14 @@ import javax.persistence.criteria.Predicate;
|
|||
import javax.persistence.criteria.Root;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.persistence.LockModeType;
|
||||
|
||||
|
@ -356,7 +358,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
|||
user.setEmailVerified(verified);
|
||||
}
|
||||
|
||||
private TypedQuery<String> createGetGroupsQuery(String search, Integer first, Integer max) {
|
||||
private TypedQuery<String> createGetGroupsQuery() {
|
||||
// we query ids only as the group might be cached and following the @ManyToOne will result in a load
|
||||
// even if we're getting just the id.
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
|
@ -365,23 +367,14 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
|||
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(builder.equal(root.get("user"), getEntity()));
|
||||
Join<UserGroupMembershipEntity, GroupEntity> join = root.join("group");
|
||||
if (Objects.nonNull(search) && !search.isEmpty()) {
|
||||
predicates.add(builder.like(builder.lower(join.get("name")), builder.lower(builder.literal("%" + search + "%"))));
|
||||
}
|
||||
|
||||
queryBuilder.select(root.get("groupId"));
|
||||
queryBuilder.where(predicates.toArray(new Predicate[0]));
|
||||
queryBuilder.orderBy(builder.asc(join.get("name")));
|
||||
|
||||
TypedQuery<String> query = em.createQuery(queryBuilder);
|
||||
if (Objects.nonNull(first) && Objects.nonNull(max)) {
|
||||
query.setFirstResult(first).setMaxResults(max);
|
||||
}
|
||||
return query;
|
||||
return em.createQuery(queryBuilder);
|
||||
}
|
||||
|
||||
private TypedQuery<Long> createCountGroupsQuery(String search) {
|
||||
private TypedQuery<Long> createCountGroupsQuery() {
|
||||
// we query ids only as the group might be cached and following the @ManyToOne will result in a load
|
||||
// even if we're getting just the id.
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
|
@ -390,38 +383,31 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
|||
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(builder.equal(root.get("user"), getEntity()));
|
||||
if (Objects.nonNull(search) && !search.isEmpty()) {
|
||||
Join<UserGroupMembershipEntity, GroupEntity> join = root.join("group");
|
||||
predicates.add(builder.like(join.get("name"), builder.literal("%" + search + "%")));
|
||||
}
|
||||
|
||||
queryBuilder.select(builder.count(root));
|
||||
queryBuilder.where(predicates.toArray(new Predicate[0]));
|
||||
return em.createQuery(queryBuilder);
|
||||
}
|
||||
|
||||
private Stream<GroupModel> getGroupModels(Stream<String> groupIds) {
|
||||
return groupIds.map(realm::getGroupById);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream() {
|
||||
return closing(getGroupModels(createGetGroupsQuery(null, null, null).getResultStream()));
|
||||
return getGroupsStream(null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(String search, int first, int max) {
|
||||
return closing(getGroupModels(createGetGroupsQuery(search, first, max).getResultStream()));
|
||||
public Stream<GroupModel> getGroupsStream(String search, Integer first, Integer max) {
|
||||
return session.groups().getGroupsStream(realm, closing(createGetGroupsQuery().getResultStream()), search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupsCount() {
|
||||
return createCountGroupsQuery(null).getSingleResult();
|
||||
return createCountGroupsQuery().getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupsCountByNameContaining(String search) {
|
||||
return createCountGroupsQuery(search).getSingleResult();
|
||||
if (search == null) return getGroupsCount();
|
||||
return session.groups().getGroupsCount(realm, closing(createGetGroupsQuery().getResultStream()), search);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -29,7 +29,11 @@ import java.util.LinkedList;
|
|||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parentId = :parent"),
|
||||
@NamedQuery(name="getGroupIdsByRealm", query="select u.id from GroupEntity u where u.realm = :realm order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm = :realm and u.name like concat('%',:search,'%') order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByNameContainingFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and u.id in :ids order by u.name ASC"),
|
||||
@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="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")
|
||||
|
|
|
@ -161,12 +161,10 @@ public class RealmEntity {
|
|||
@JoinTable(name="REALM_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
|
||||
protected Collection<RoleEntity> defaultRoles;
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
|
||||
@JoinTable(name="REALM_DEFAULT_GROUPS", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="GROUP_ID")})
|
||||
protected Collection<GroupEntity> defaultGroups;
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
|
||||
protected Collection<GroupEntity> groups;
|
||||
@ElementCollection
|
||||
@Column(name="GROUP_ID")
|
||||
@CollectionTable(name="REALM_DEFAULT_GROUPS", joinColumns={ @JoinColumn(name="REALM_ID") })
|
||||
protected Set<String> defaultGroupIds;
|
||||
|
||||
@Column(name="EVENTS_ENABLED")
|
||||
protected boolean eventsEnabled;
|
||||
|
@ -467,26 +465,15 @@ public class RealmEntity {
|
|||
this.defaultRoles = defaultRoles;
|
||||
}
|
||||
|
||||
public Collection<GroupEntity> getDefaultGroups() {
|
||||
if (defaultGroups == null) {
|
||||
defaultGroups = new LinkedList<>();
|
||||
public Set<String> getDefaultGroupIds() {
|
||||
if (defaultGroupIds == null) {
|
||||
defaultGroupIds = new HashSet<>();
|
||||
}
|
||||
return defaultGroups;
|
||||
return defaultGroupIds;
|
||||
}
|
||||
|
||||
public void setDefaultGroups(Collection<GroupEntity> defaultGroups) {
|
||||
this.defaultGroups = defaultGroups;
|
||||
}
|
||||
|
||||
public Collection<GroupEntity> getGroups() {
|
||||
if (groups == null) {
|
||||
groups = new LinkedList<>();
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(Collection<GroupEntity> groups) {
|
||||
this.groups = groups;
|
||||
public void setDefaultGroupIds(Set<String> defaultGroups) {
|
||||
this.defaultGroupIds = defaultGroups;
|
||||
}
|
||||
|
||||
public String getPasswordPolicy() {
|
||||
|
|
|
@ -42,8 +42,8 @@ import java.io.Serializable;
|
|||
@NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"),
|
||||
@NamedQuery(name="deleteUserGroupMembershipsByUser", query="delete from UserGroupMembershipEntity m where m.user = :user"),
|
||||
@NamedQuery(name="searchForUserCountInGroups", query="select count(m.user) from UserGroupMembershipEntity m where m.user.realmId = :realmId and (m.user.serviceAccountClientLink is null) and " +
|
||||
"( lower(m.user.username) like :search or lower(concat(m.user.firstName, ' ', m.user.lastName)) like :search or m.user.email like :search ) and m.group.id in :groupIds"),
|
||||
@NamedQuery(name="userCountInGroups", query="select count(m.user) from UserGroupMembershipEntity m where m.user.realmId = :realmId and m.group.id in :groupIds")
|
||||
"( lower(m.user.username) like :search or lower(concat(m.user.firstName, ' ', m.user.lastName)) like :search or m.user.email like :search ) and m.groupId in :groupIds"),
|
||||
@NamedQuery(name="userCountInGroups", query="select count(m.user) from UserGroupMembershipEntity m where m.user.realmId = :realmId and m.groupId in :groupIds")
|
||||
})
|
||||
@Table(name="USER_GROUP_MEMBERSHIP")
|
||||
@Entity
|
||||
|
@ -55,11 +55,6 @@ public class UserGroupMembershipEntity {
|
|||
@JoinColumn(name="USER_ID")
|
||||
protected UserEntity user;
|
||||
|
||||
@Id
|
||||
@ManyToOne(fetch= FetchType.LAZY)
|
||||
@JoinColumn(name="GROUP_ID", insertable=false, updatable=false)
|
||||
protected GroupEntity group;
|
||||
|
||||
@Id
|
||||
@Column(name = "GROUP_ID")
|
||||
protected String groupId;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
~ * Copyright 2020 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.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
|
||||
<changeSet author="keycloak" id="map-remove-ri">
|
||||
<dropForeignKeyConstraint baseTableName="REALM_DEFAULT_GROUPS" constraintName="FK_DEF_GROUPS_GROUP"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
|
@ -67,5 +67,6 @@
|
|||
<include file="META-INF/jpa-changelog-9.0.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-9.0.1.xml"/>
|
||||
<include file="META-INF/jpa-changelog-11.0.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-12.0.0.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author mhajas
|
||||
*/
|
||||
public abstract class AbstractGroupEntity<K> implements AbstractEntity<K> {
|
||||
|
||||
private final K id;
|
||||
private final String realmId;
|
||||
|
||||
private String name;
|
||||
private String parentId;
|
||||
private Map<String, List<String>> attributes = new HashMap<>();
|
||||
private Set<String> grantedRoles = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Flag signalizing that any of the setters has been meaningfully used.
|
||||
*/
|
||||
protected boolean updated;
|
||||
|
||||
protected AbstractGroupEntity() {
|
||||
this.id = null;
|
||||
this.realmId = null;
|
||||
}
|
||||
|
||||
public AbstractGroupEntity(K id, String realmId) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
Objects.requireNonNull(realmId, "realmId");
|
||||
|
||||
this.id = id;
|
||||
this.realmId = realmId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdated() {
|
||||
return this.updated;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.updated |= ! Objects.equals(this.name, name);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(String parentId) {
|
||||
this.updated |= !Objects.equals(this.parentId, parentId);
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
this.updated |= ! Objects.equals(this.attributes, attributes);
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public void setAttribute(String name, List<String> value) {
|
||||
this.updated |= !this.attributes.containsKey(name) || !this.attributes.get(name).equals(value);
|
||||
this.attributes.put(name, value);
|
||||
}
|
||||
|
||||
public void removeAttribute(String name) {
|
||||
this.updated |= this.attributes.remove(name) != null;
|
||||
}
|
||||
|
||||
public List<String> getAttribute(String name) {
|
||||
return this.attributes.get(name);
|
||||
}
|
||||
|
||||
public String getRealmId() {
|
||||
return this.realmId;
|
||||
}
|
||||
|
||||
public Set<String> getGrantedRoles() {
|
||||
return grantedRoles;
|
||||
}
|
||||
|
||||
public void setGrantedRoles(Set<String> grantedRoles) {
|
||||
this.updated |= !Objects.equals(this.grantedRoles, grantedRoles);
|
||||
this.grantedRoles = grantedRoles;
|
||||
}
|
||||
|
||||
public void removeRole(String role) {
|
||||
this.updated |= this.grantedRoles.contains(role);
|
||||
this.grantedRoles.remove(role);
|
||||
}
|
||||
|
||||
public void addGrantedRole(String role) {
|
||||
this.updated |= !this.grantedRoles.contains(role);
|
||||
this.grantedRoles.add(role);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class AbstractGroupModel<E extends AbstractEntity> implements GroupModel {
|
||||
|
||||
protected final KeycloakSession session;
|
||||
protected final RealmModel realm;
|
||||
protected final E entity;
|
||||
|
||||
public AbstractGroupModel(KeycloakSession session, RealmModel realm, E entity) {
|
||||
Objects.requireNonNull(entity, "entity");
|
||||
Objects.requireNonNull(realm, "realm");
|
||||
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GroupModel)) return false;
|
||||
|
||||
GroupModel that = (GroupModel) o;
|
||||
return Objects.equals(that.getId(), getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getId().hashCode();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
public class MapGroupAdapter extends AbstractGroupModel<MapGroupEntity> {
|
||||
public MapGroupAdapter(KeycloakSession session, RealmModel realm, MapGroupEntity entity) {
|
||||
super(session, realm, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return entity.getId().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return entity.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
entity.setName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleAttribute(String name, String value) {
|
||||
entity.setAttribute(name, Collections.singletonList(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, List<String> values) {
|
||||
entity.setAttribute(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name) {
|
||||
entity.removeAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstAttribute(String name) {
|
||||
List<String> attributeValues = this.entity.getAttribute(name);
|
||||
if (attributeValues == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributeValues.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> getAttributeStream(String name) {
|
||||
List<String> attributes = entity.getAttribute(name);
|
||||
if (attributes == null || attributes.isEmpty()) return Stream.empty();
|
||||
return attributes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return entity.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupModel getParent() {
|
||||
String parentId = getParentId();
|
||||
if (parentId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.groups().getGroupById(realm, parentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentId() {
|
||||
return entity.getParentId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getSubGroupsStream() {
|
||||
return session.groups().getGroupsStream(realm)
|
||||
.filter(groupModel -> getId().equals(groupModel.getParentId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParent(GroupModel group) {
|
||||
if (group == null) {
|
||||
entity.setParentId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getId().equals(group.getId())) {
|
||||
entity.setParentId(group.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChild(GroupModel subGroup) {
|
||||
subGroup.setParent(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeChild(GroupModel subGroup) {
|
||||
if (getId().equals(subGroup.getParentId())) {
|
||||
subGroup.setParent(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRealmRoleMappingsStream() {
|
||||
return getRoleMappingsStream()
|
||||
.filter(roleModel -> roleModel.getContainer() instanceof RealmModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getClientRoleMappingsStream(ClientModel app) {
|
||||
final String clientId = app.getId();
|
||||
return getRoleMappingsStream()
|
||||
.filter(roleModel -> roleModel.getContainer() instanceof ClientModel)
|
||||
.filter(roleModel -> roleModel.getContainer().getId().equals(clientId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(RoleModel role) {
|
||||
return entity.getGrantedRoles().contains(role.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantRole(RoleModel role) {
|
||||
entity.addGrantedRole(role.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRoleMappingsStream() {
|
||||
return entity.getGrantedRoles().stream()
|
||||
.map(roleId -> session.roles().getRoleById(realm, roleId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRoleMapping(RoleModel role) {
|
||||
entity.removeRole(role.getId());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class MapGroupEntity extends AbstractGroupEntity<UUID> {
|
||||
|
||||
protected MapGroupEntity() {
|
||||
super();
|
||||
}
|
||||
|
||||
public MapGroupEntity(UUID id, String realmId) {
|
||||
super(id, realmId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.map.common.Serialization;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||
|
||||
public class MapGroupProvider implements GroupProvider {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MapGroupProvider.class);
|
||||
private static final Predicate<MapGroupEntity> ALWAYS_FALSE = c -> { return false; };
|
||||
private final KeycloakSession session;
|
||||
final MapKeycloakTransaction<UUID, MapGroupEntity> tx;
|
||||
private final MapStorage<UUID, MapGroupEntity> groupStore;
|
||||
|
||||
public MapGroupProvider(KeycloakSession session, MapStorage<UUID, MapGroupEntity> groupStore) {
|
||||
this.session = session;
|
||||
this.groupStore = groupStore;
|
||||
this.tx = new MapKeycloakTransaction<>(groupStore);
|
||||
session.getTransactionManager().enlist(tx);
|
||||
}
|
||||
|
||||
private MapGroupEntity registerEntityForChanges(MapGroupEntity origEntity) {
|
||||
final MapGroupEntity res = Serialization.from(origEntity);
|
||||
tx.putIfChanged(origEntity.getId(), res, MapGroupEntity::isUpdated);
|
||||
return res;
|
||||
}
|
||||
|
||||
private Function<MapGroupEntity, GroupModel> entityToAdapterFunc(RealmModel realm) {
|
||||
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
|
||||
return origEntity -> new MapGroupAdapter(session, realm, registerEntityForChanges(origEntity));
|
||||
}
|
||||
|
||||
private Predicate<MapGroupEntity> entityRealmFilter(RealmModel realm) {
|
||||
if (realm == null || realm.getId() == null) {
|
||||
return MapGroupProvider.ALWAYS_FALSE;
|
||||
}
|
||||
String realmId = realm.getId();
|
||||
return entity -> Objects.equals(realmId, entity.getRealmId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupModel getGroupById(RealmModel realm, String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.tracef("getGroupById(%s, %s)%s", realm, id, getShortStackTrace());
|
||||
|
||||
|
||||
UUID uid;
|
||||
try {
|
||||
uid = UUID.fromString(id);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MapGroupEntity entity = tx.get(uid, groupStore::get);
|
||||
return (entity == null || ! entityRealmFilter(realm).test(entity))
|
||||
? null
|
||||
: entityToAdapterFunc(realm).apply(entity);
|
||||
}
|
||||
|
||||
private Stream<MapGroupEntity> getNotRemovedUpdatedGroupsStream() {
|
||||
Stream<MapGroupEntity> updatedAndNotRemovedGroupsStream = groupStore.entrySet().stream()
|
||||
.map(tx::getUpdated) // If the group has been removed, tx.get will return null, otherwise it will return me.getValue()
|
||||
.filter(Objects::nonNull);
|
||||
return Stream.concat(tx.createdValuesStream(groupStore.keySet()), updatedAndNotRemovedGroupsStream);
|
||||
}
|
||||
|
||||
private Stream<MapGroupEntity> getUnsortedGroupEntitiesStream(RealmModel realm) {
|
||||
return getNotRemovedUpdatedGroupsStream()
|
||||
.filter(entityRealmFilter(realm));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm) {
|
||||
LOG.tracef("getGroupsStream(%s)%s", realm, getShortStackTrace());
|
||||
return getUnsortedGroupEntitiesStream(realm)
|
||||
.map(entityToAdapterFunc(realm))
|
||||
.sorted(GroupModel.COMPARE_BY_NAME)
|
||||
;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
|
||||
Stream<GroupModel> groupModelStream = ids.map(id -> session.groups().getGroupById(realm, id))
|
||||
.sorted(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
if (search != null) {
|
||||
String s = search.toLowerCase();
|
||||
groupModelStream = groupModelStream.filter(groupModel -> groupModel.getName().toLowerCase().contains(s));
|
||||
}
|
||||
|
||||
if (first != null && first > 0) {
|
||||
groupModelStream = groupModelStream.skip(first);
|
||||
}
|
||||
|
||||
if (max != null && max >= 0) {
|
||||
groupModelStream = groupModelStream.limit(max);
|
||||
}
|
||||
|
||||
return groupModelStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
|
||||
LOG.tracef("getGroupsCount(%s, %s)%s", realm, onlyTopGroups, getShortStackTrace());
|
||||
Stream<MapGroupEntity> groupModelStream = getUnsortedGroupEntitiesStream(realm);
|
||||
|
||||
if (onlyTopGroups) {
|
||||
groupModelStream = groupModelStream.filter(groupEntity -> Objects.isNull(groupEntity.getParentId()));
|
||||
}
|
||||
|
||||
return groupModelStream.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
|
||||
return searchForGroupByNameStream(realm, search, null, null).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||
LOG.tracef("getGroupsByRole(%s, %s, %d, %d)%s", realm, role, firstResult, maxResults, getShortStackTrace());
|
||||
Stream<GroupModel> groupModelStream = getGroupsStream(realm).filter(groupModel -> groupModel.hasRole(role));
|
||||
|
||||
if (firstResult != null && firstResult > 0) {
|
||||
groupModelStream = groupModelStream.skip(firstResult);
|
||||
}
|
||||
|
||||
if (maxResults != null && maxResults >= 0) {
|
||||
groupModelStream = groupModelStream.limit(maxResults);
|
||||
}
|
||||
|
||||
return groupModelStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm) {
|
||||
LOG.tracef("getTopLevelGroupsStream(%s)%s", realm, getShortStackTrace());
|
||||
return getGroupsStream(realm)
|
||||
.filter(groupModel -> Objects.isNull(groupModel.getParentId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer firstResult, Integer maxResults) {
|
||||
Stream<GroupModel> groupModelStream = getTopLevelGroupsStream(realm);
|
||||
|
||||
if (firstResult != null && firstResult > 0) {
|
||||
groupModelStream = groupModelStream.skip(firstResult);
|
||||
}
|
||||
|
||||
if (maxResults != null && maxResults >= 0) {
|
||||
groupModelStream = groupModelStream.limit(maxResults);
|
||||
}
|
||||
|
||||
return groupModelStream;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
||||
LOG.tracef("searchForGroupByNameStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace());
|
||||
Stream<GroupModel> groupModelStream = getGroupsStream(realm)
|
||||
.filter(groupModel -> groupModel.getName().contains(search));
|
||||
|
||||
if (firstResult != null && firstResult > 0) {
|
||||
groupModelStream = groupModelStream.skip(firstResult);
|
||||
}
|
||||
|
||||
if (maxResults != null && maxResults >= 0) {
|
||||
groupModelStream = groupModelStream.limit(maxResults);
|
||||
}
|
||||
|
||||
return groupModelStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
|
||||
LOG.tracef("createGroup(%s, %s, %s, %s)%s", realm, id, name, toParent, getShortStackTrace());
|
||||
final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id);
|
||||
|
||||
// Check Db constraint: uniqueConstraints = { @UniqueConstraint(columnNames = {"REALM_ID", "PARENT_GROUP", "NAME"})}
|
||||
if (getUnsortedGroupEntitiesStream(realm)
|
||||
.anyMatch(groupEntity ->
|
||||
Objects.equals(groupEntity.getParentId(), toParent == null ? null : toParent.getId()) &&
|
||||
Objects.equals(groupEntity.getName(), name))) {
|
||||
throw new ModelDuplicateException("Group with name '" + name + "' in realm " + realm.getName() + " already exists for requested parent" );
|
||||
}
|
||||
|
||||
MapGroupEntity entity = new MapGroupEntity(entityId, realm.getId());
|
||||
entity.setName(name);
|
||||
entity.setParentId(toParent == null ? null : toParent.getId());
|
||||
if (tx.get(entity.getId(), groupStore::get) != null) {
|
||||
throw new ModelDuplicateException("Group exists: " + entityId);
|
||||
}
|
||||
tx.putIfAbsent(entity.getId(), entity);
|
||||
|
||||
return entityToAdapterFunc(realm).apply(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGroup(RealmModel realm, GroupModel group) {
|
||||
LOG.tracef("removeGroup(%s, %s)%s", realm, group, getShortStackTrace());
|
||||
if (group == null) return false;
|
||||
|
||||
// TODO: Sending an event (, user group removal and realm default groups) should be extracted to store layer
|
||||
session.getKeycloakSessionFactory().publish(new GroupModel.GroupRemovedEvent() {
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupModel getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
|
||||
session.users().preRemove(realm, group);
|
||||
realm.removeDefaultGroup(group);
|
||||
|
||||
group.getSubGroupsStream().forEach(subGroup -> session.groups().removeGroup(realm, subGroup));
|
||||
|
||||
// TODO: ^^^^^^^ Up to here
|
||||
|
||||
tx.remove(UUID.fromString(group.getId()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* TODO: investigate following two methods, it seems they could be moved to model layer */
|
||||
|
||||
@Override
|
||||
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
|
||||
LOG.tracef("moveGroup(%s, %s, %s)%s", realm, group, toParent, getShortStackTrace());
|
||||
|
||||
if (toParent != null && group.getId().equals(toParent.getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String parentId = toParent == null ? null : toParent.getId();
|
||||
Stream<MapGroupEntity> possibleSiblings = getUnsortedGroupEntitiesStream(realm)
|
||||
.filter(mapGroupEntity -> Objects.equals(mapGroupEntity.getParentId(), parentId));
|
||||
|
||||
if (possibleSiblings.map(MapGroupEntity::getName).anyMatch(Predicate.isEqual(group.getName()))) {
|
||||
throw new ModelDuplicateException("Parent already contains subgroup named '" + group.getName() + "'");
|
||||
}
|
||||
|
||||
if (group.getParentId() != null) {
|
||||
group.getParent().removeChild(group);
|
||||
}
|
||||
group.setParent(toParent);
|
||||
if (toParent != null) toParent.addChild(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
|
||||
LOG.tracef("addTopLevelGroup(%s, %s)%s", realm, subGroup, getShortStackTrace());
|
||||
|
||||
Stream<MapGroupEntity> possibleSiblings = getUnsortedGroupEntitiesStream(realm)
|
||||
.filter(mapGroupEntity -> mapGroupEntity.getParentId() == null);
|
||||
|
||||
if (possibleSiblings.map(MapGroupEntity::getName).anyMatch(Predicate.isEqual(subGroup.getName()))) {
|
||||
throw new ModelDuplicateException("There is already a top level group named '" + subGroup.getName() + "'");
|
||||
}
|
||||
|
||||
subGroup.setParent(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm, RoleModel role) {
|
||||
LOG.tracef("preRemove(%s, %s)%s", realm, role, getShortStackTrace());
|
||||
final String roleId = role.getId();
|
||||
getUnsortedGroupEntitiesStream(realm)
|
||||
.filter(groupEntity -> groupEntity.getGrantedRoles().contains(roleId))
|
||||
.map(groupEntity -> session.groups().getGroupById(realm, groupEntity.getId().toString()))
|
||||
.forEach(groupModel -> groupModel.deleteRoleMapping(role));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.group;
|
||||
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.GroupProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.map.common.AbstractMapProviderFactory;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author mhajas
|
||||
*/
|
||||
public class MapGroupProviderFactory extends AbstractMapProviderFactory<GroupProvider> implements GroupProviderFactory {
|
||||
|
||||
private MapStorage<UUID, MapGroupEntity> store;
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class);
|
||||
this.store = sp.getStorage("groups", UUID.class, MapGroupEntity.class);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GroupProvider create(KeycloakSession session) {
|
||||
return new MapGroupProvider(session, store);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# Copyright 2020 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.map.group.MapGroupProviderFactory
|
|
@ -19,6 +19,7 @@ package org.keycloak.models;
|
|||
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -35,6 +36,9 @@ public interface GroupModel extends RoleMapperModel {
|
|||
GroupModel getGroup();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
Comparator<GroupModel> COMPARE_BY_NAME = Comparator.comparing(GroupModel::getName);
|
||||
|
||||
String getId();
|
||||
|
||||
String getName();
|
||||
|
|
|
@ -64,6 +64,56 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
*/
|
||||
Stream<GroupModel> getGroupsStream(RealmModel realm);
|
||||
|
||||
/**
|
||||
* Returns a list of groups with given ids.
|
||||
* Effectively the same as {@code getGroupsStream(realm, ids, null, null, null)}.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param ids List of ids.
|
||||
* @return List of GroupModels with the specified ids
|
||||
*/
|
||||
default Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids) {
|
||||
return getGroupsStream(realm, ids, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a paginated stream of groups with given ids and given search value in group names.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param ids List of ids.
|
||||
* @param search Case insensitive string which will be searched for. Ignored if null.
|
||||
* @param first Index of the first result to return. Ignored if negative or {@code null}.
|
||||
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return List of desired groups.
|
||||
*/
|
||||
Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max);
|
||||
|
||||
/**
|
||||
* Returns a paginated list of groups with given ids.
|
||||
* Effectively the same as {@code getGroupsStream(realm, ids, null, first, max)}.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param ids List of ids.
|
||||
* @param first Index of the first result to return. Ignored if negative or {@code null}.
|
||||
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return List of GroupModels with the specified ids
|
||||
*/
|
||||
default Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, Integer first, Integer max) {
|
||||
return getGroupsStream(realm, ids, null, first, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of groups that contains the search string in the name
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param ids List of ids.
|
||||
* @param search Case insensitive string which will be searched for. Ignored if null.
|
||||
* @return Number of groups.
|
||||
*/
|
||||
default Long getGroupsCount(RealmModel realm, Stream<String> ids, String search) {
|
||||
return getGroupsStream(realm, ids, search, null, null).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of groups/top level groups (i.e. groups without parent group) for the given realm.
|
||||
*
|
||||
|
@ -77,7 +127,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* Returns number of groups with the given string in name for the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param search Searched string.
|
||||
* @param search Case insensitive string which will be searched for.
|
||||
* @return Number of groups with the given string in its name.
|
||||
*/
|
||||
Long getGroupsCountByNameContaining(RealmModel realm, String search);
|
||||
|
@ -90,7 +140,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @param firstResult First result to return. Ignored if negative.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative.
|
||||
* @return List of groups with the given role.
|
||||
* @deprecated Use {@link #getGroupsByRoleStream(RealmModel, RoleModel, int, int)} getGroupsByRoleStream} instead.
|
||||
* @deprecated Use {@link #getGroupsByRoleStream(RealmModel, RoleModel, Integer, Integer)} getGroupsByRoleStream} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
default List<GroupModel> getGroupsByRole(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
|
||||
|
@ -102,11 +152,11 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
*
|
||||
* @param realm Realm.
|
||||
* @param role Role.
|
||||
* @param firstResult First result to return. Ignored if negative.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative.
|
||||
* @param firstResult First result to return. Ignored if negative or {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return Stream of groups with the given role.
|
||||
*/
|
||||
Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, int firstResult, int maxResults);
|
||||
Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults);
|
||||
|
||||
/**
|
||||
* Returns all top level groups (i.e. groups without parent group) for the given realm.
|
||||
|
@ -132,8 +182,8 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* Returns top level groups (i.e. groups without parent group) for the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param firstResult First result to return.
|
||||
* @param maxResults Maximum number of results to return.
|
||||
* @param firstResult First result to return. Ignored if negative or {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return List of top level groups in the realm.
|
||||
* @deprecated Use {@link #getTopLevelGroupsStream(RealmModel, Integer, Integer)} getTopLevelGroupsStream} instead.
|
||||
*/
|
||||
|
@ -146,8 +196,8 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* Returns top level groups (i.e. groups without parent group) for the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param firstResult First result to return.
|
||||
* @param maxResults Maximum number of results to return.
|
||||
* @param firstResult First result to return. Ignored if negative or {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return Stream of top level groups in the realm.
|
||||
*/
|
||||
Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer firstResult, Integer maxResults);
|
||||
|
@ -158,6 +208,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
*
|
||||
* @param realm Realm.
|
||||
* @param name Name.
|
||||
* @throws ModelDuplicateException If there is already a top-level group with the given name
|
||||
* @return Model of the created group.
|
||||
*/
|
||||
default GroupModel createGroup(RealmModel realm, String name) {
|
||||
|
@ -171,6 +222,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @param realm Realm.
|
||||
* @param id Id.
|
||||
* @param name Name.
|
||||
* @throws ModelDuplicateException If a group with given id already exists or there is a top-level group with the given name
|
||||
* @return Model of the created group
|
||||
*/
|
||||
default GroupModel createGroup(RealmModel realm, String id, String name) {
|
||||
|
@ -184,6 +236,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @param realm Realm.
|
||||
* @param name Name.
|
||||
* @param toParent Parent group.
|
||||
* @throws ModelDuplicateException If the toParent group already has a subgroup with the given name
|
||||
* @return Model of the created group.
|
||||
*/
|
||||
default GroupModel createGroup(RealmModel realm, String name, GroupModel toParent) {
|
||||
|
@ -197,6 +250,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @param id Id, will be generated if {@code null}.
|
||||
* @param name Name.
|
||||
* @param toParent Parent group, or {@code null} if the group is top level group
|
||||
* @throws ModelDuplicateException If a group with the given id already exists or the toParent group has a subgroup with the given name
|
||||
* @return Model of the created group
|
||||
*/
|
||||
GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent);
|
||||
|
@ -221,6 +275,7 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @param realm Realm owning this group.
|
||||
* @param group Group to update.
|
||||
* @param toParent New parent group, or {@code null} if we are moving the group to top level group.
|
||||
* @throws ModelDuplicateException If there is already a group with group.name under the toParent group (or top-level if toParent is null)
|
||||
*/
|
||||
void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent);
|
||||
|
||||
|
@ -229,6 +284,15 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
*
|
||||
* @param realm Realm.
|
||||
* @param subGroup Group.
|
||||
* @throws ModelDuplicateException If there is already a top level group name with the same name
|
||||
*/
|
||||
void addTopLevelGroup(RealmModel realm, GroupModel subGroup);
|
||||
|
||||
/**
|
||||
* This function is called when a role is removed; this serves for removing references from groups to roles.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param role Role which will be removed.
|
||||
*/
|
||||
void preRemove(RealmModel realm, RoleModel role);
|
||||
}
|
||||
|
|
|
@ -137,11 +137,29 @@ public interface UserModel extends RoleMapperModel {
|
|||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
default Stream<GroupModel> getGroupsStream(String search, int first, int max) {
|
||||
return getGroupsStream()
|
||||
.filter(group -> search == null || group.getName().toLowerCase().contains(search.toLowerCase()))
|
||||
.skip(first)
|
||||
.limit(max);
|
||||
/**
|
||||
* Returns a paginated stream of groups within this.realm with search in the name
|
||||
*
|
||||
* @param search Case insensitive string which will be searched for. Ignored if null.
|
||||
* @param first Index of first group to return. Ignored if negative or {@code null}.
|
||||
* @param max Maximum number of records to return. Ignored if negative or {@code null}.
|
||||
* @return Stream of desired groups.
|
||||
*/
|
||||
default Stream<GroupModel> getGroupsStream(String search, Integer first, Integer max) {
|
||||
if (search != null) search = search.toLowerCase();
|
||||
final String finalSearch = search;
|
||||
Stream<GroupModel> groupModelStream = getGroupsStream()
|
||||
.filter(group -> finalSearch == null || group.getName().toLowerCase().contains(finalSearch));
|
||||
|
||||
if (first != null && first > 0) {
|
||||
groupModelStream = groupModelStream.skip(first);
|
||||
}
|
||||
|
||||
if (max != null && max >= 0) {
|
||||
groupModelStream = groupModelStream.limit(max);
|
||||
}
|
||||
|
||||
return groupModelStream;
|
||||
}
|
||||
|
||||
default long getGroupsCount() {
|
||||
|
|
|
@ -38,7 +38,7 @@ public interface GroupLookupProvider {
|
|||
* Returns groups with the given string in name for the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param search Searched string.
|
||||
* @param search Case sensitive searched string.
|
||||
* @param firstResult First result to return. Ignored if {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
|
||||
* @return List of groups with the given string in name.
|
||||
|
@ -53,9 +53,9 @@ public interface GroupLookupProvider {
|
|||
* Returns groups with the given string in name for the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param search Searched string.
|
||||
* @param firstResult First result to return. Ignored if {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
|
||||
* @param search Case sensitive searched string.
|
||||
* @param firstResult First result to return. Ignored if negative or {@code null}.
|
||||
* @param maxResults Maximum number of results to return. Ignored if negative or {@code null}.
|
||||
* @return Stream of groups with the given string in name.
|
||||
*/
|
||||
Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.common.util.Resteasy;
|
|||
import org.keycloak.forms.login.freemarker.model.UrlBean;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
|
@ -107,6 +108,11 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
|
|||
if (throwable instanceof JsonParseException) {
|
||||
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||
}
|
||||
|
||||
if (throwable instanceof ModelDuplicateException) {
|
||||
status = Response.Status.CONFLICT.getStatusCode();
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
|
|
@ -146,10 +146,6 @@ public class GroupResource {
|
|||
public Response addChild(GroupRepresentation rep) {
|
||||
this.auth.groups().requireManage(group);
|
||||
|
||||
if (group.getSubGroupsStream().map(GroupModel::getName).anyMatch(Predicate.isEqual(rep.getName()))) {
|
||||
return ErrorResponse.exists("Parent already contains subgroup named '" + rep.getName() + "'");
|
||||
}
|
||||
|
||||
Response.ResponseBuilder builder = Response.status(204);
|
||||
GroupModel child = null;
|
||||
if (rep.getId() != null) {
|
||||
|
|
|
@ -74,6 +74,11 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
|
|||
return session.groupLocalStorage().getGroupsStream(realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
|
||||
return session.groupLocalStorage().getGroupsStream(realm, ids, search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
|
||||
return session.groupLocalStorage().getGroupsCount(realm, onlyTopGroups);
|
||||
|
@ -85,7 +90,7 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
|
|||
}
|
||||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
|
||||
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||
return session.groupLocalStorage().getGroupsByRoleStream(realm, role, firstResult, maxResults);
|
||||
}
|
||||
|
||||
|
@ -119,6 +124,11 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
|
|||
session.groupLocalStorage().addTopLevelGroup(realm, subGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm, RoleModel role) {
|
||||
session.groupLocalStorage().preRemove(realm, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.arquillian;
|
||||
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jboss.arquillian.container.spi.ContainerRegistry;
|
||||
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
|
||||
|
@ -66,6 +67,7 @@ import org.wildfly.extras.creaper.core.online.operations.Operations;
|
|||
import org.wildfly.extras.creaper.core.online.operations.admin.Administration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
@ -326,6 +328,17 @@ public class AuthServerTestEnricher {
|
|||
CrossDCTestEnricher.initializeSuiteContext(suiteContext);
|
||||
log.info("\n\n" + suiteContext);
|
||||
log.info("\n\n" + SystemInfoHelper.getSystemInfo());
|
||||
|
||||
// Remove all map storages present in target directory
|
||||
// This is useful for example in intellij where target directory is not removed between test runs
|
||||
File dir = new File(System.getProperty("project.build.directory", "target"));
|
||||
FileFilter fileFilter = new WildcardFileFilter("map-*.json");
|
||||
File[] files = dir.listFiles(fileFilter);
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ContainerInfo updateWithAuthServerInfo(ContainerInfo authServerInfo) {
|
||||
|
|
|
@ -56,6 +56,7 @@ import java.util.Map;
|
|||
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -176,6 +177,7 @@ public class UserStorageConsentTest extends AbstractServletsAdapterTest {
|
|||
.build("demo").toString();
|
||||
|
||||
driver.navigate().to(logoutUri);
|
||||
waitForPageToLoad();
|
||||
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
|
||||
productPortal.navigateTo();
|
||||
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
|
||||
|
|
|
@ -2226,10 +2226,12 @@ public class UserTest extends AbstractAdminTest {
|
|||
}
|
||||
|
||||
List<GroupRepresentation> groups = realm.users().get(userId).groups("-3", 0, 10);
|
||||
assertThat(realm.users().get(userId).groupsCount("-3").get("count"), is(1L));
|
||||
assertEquals(1, groups.size());
|
||||
assertNames(groups, "group-3");
|
||||
|
||||
List<GroupRepresentation> groups2 = realm.users().get(userId).groups("1", 0, 10);
|
||||
assertThat(realm.users().get(userId).groupsCount("1").get("count"), is(2L));
|
||||
assertEquals(2, groups2.size());
|
||||
assertNames(groups2, "group-1", "group-10");
|
||||
|
||||
|
@ -2237,6 +2239,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
assertEquals(0, groups3.size());
|
||||
|
||||
List<GroupRepresentation> groups4 = realm.users().get(userId).groups("gr", 2, 10);
|
||||
assertThat(realm.users().get(userId).groupsCount("gr").get("count"), is(10L));
|
||||
assertEquals(8, groups4.size());
|
||||
|
||||
List<GroupRepresentation> groups5 = realm.users().get(userId).groups("Gr", 2, 10);
|
||||
|
|
|
@ -233,7 +233,7 @@ public class ConcurrencyTest extends AbstractConcurrencyTest {
|
|||
|
||||
c = realm.groups().group(id).toRepresentation();
|
||||
assertNotNull(c);
|
||||
assertTrue("Group " + name + " not found in group list",
|
||||
assertTrue("Group " + name + " [" + id + "] " + " not found in group list",
|
||||
realm.groups().groups().stream()
|
||||
.map(GroupRepresentation::getName)
|
||||
.filter(Objects::nonNull)
|
||||
|
|
|
@ -515,6 +515,18 @@ public class GroupTest extends AbstractGroupTest {
|
|||
assertNames(group1.getSubGroups(), "mygroup2");
|
||||
Assert.assertEquals("/mygroup1/mygroup2", group2.getPath());
|
||||
|
||||
assertAdminEvents.clear();
|
||||
|
||||
// Create top level group with the same name
|
||||
group = GroupBuilder.create()
|
||||
.name("mygroup2")
|
||||
.build();
|
||||
GroupRepresentation group3 = createGroup(realm, group);
|
||||
// Try to move top level "mygroup2" as child of "mygroup1". It should fail as there is already a child group
|
||||
// of "mygroup1" with name "mygroup2"
|
||||
response = realm.groups().group(group1.getId()).subGroup(group3);
|
||||
Assert.assertEquals(409, response.getStatus());
|
||||
realm.groups().group(group3.getId()).remove();
|
||||
|
||||
// Move "mygroup2" back under parent
|
||||
response = realm.groups().add(group2);
|
||||
|
|
|
@ -44,6 +44,9 @@ import org.keycloak.testsuite.admin.ApiUtil;
|
|||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class LDAPGroupMapperSyncWithGroupsPathTest extends AbstractLDAPTest {
|
||||
|
||||
|
@ -92,7 +95,11 @@ public class LDAPGroupMapperSyncWithGroupsPathTest extends AbstractLDAPTest {
|
|||
RealmModel realm = ctx.getRealm();
|
||||
|
||||
GroupModel groupsPathGroup = KeycloakModelUtils.findGroupByPath(realm, LDAP_GROUPS_PATH);
|
||||
groupsPathGroup.getSubGroupsStream().forEach(realm::removeGroup);
|
||||
|
||||
// Subgroup stream needs to be collected, because otherwise we can end up with finding group with id that is
|
||||
// already removed
|
||||
groupsPathGroup.getSubGroupsStream().collect(Collectors.toSet())
|
||||
.forEach(realm::removeGroup);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -653,11 +654,12 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
|
|||
|
||||
@Test
|
||||
public void testHardcodedGroupMapper() {
|
||||
final String uuid = UUID.randomUUID().toString();
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
GroupModel hardcodedGroup = appRealm.createGroup("hardcoded-group", "hardcoded-group");
|
||||
GroupModel hardcodedGroup = appRealm.createGroup(uuid, "hardcoded-group");
|
||||
|
||||
// assert that user "johnkeycloak" doesn't have hardcoded group
|
||||
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||
|
@ -673,7 +675,7 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
|
|||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
GroupModel hardcodedGroup = appRealm.getGroupById("hardcoded-group");
|
||||
GroupModel hardcodedGroup = appRealm.getGroupById(uuid);
|
||||
|
||||
// Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
|
||||
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||
|
|
Loading…
Reference in a new issue