Filtering organization groups when managing or processing groups
Closes #30589 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
657aff787f
commit
cc2ccc87b0
28 changed files with 395 additions and 242 deletions
|
@ -290,4 +290,10 @@ public class GroupAdapter implements GroupModel {
|
|||
public boolean escapeSlashesInGroupPath() {
|
||||
return KeycloakModelUtils.escapeSlashesInGroupPath(keycloakSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
if (isUpdated()) return updated.getType();
|
||||
return cached.getType();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.models.ClientProvider;
|
|||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.ClientScopeProvider;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
|
@ -1170,8 +1171,8 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
|
||||
GroupModel group = getGroupDelegate().createGroup(realm, id, name, toParent);
|
||||
public GroupModel createGroup(RealmModel realm, String id, Type type, String name, GroupModel toParent) {
|
||||
GroupModel group = getGroupDelegate().createGroup(realm, id, type, name, toParent);
|
||||
return groupAdded(realm, group, toParent);
|
||||
}
|
||||
|
||||
|
@ -1603,4 +1604,10 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm) {
|
||||
listInvalidations.add(realm.getId());
|
||||
getGroupDelegate().preRemove(realm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.cache.infinispan;
|
|||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
@ -31,8 +32,8 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -395,19 +396,33 @@ public class UserAdapter implements CachedUserModel {
|
|||
|
||||
@Override
|
||||
public Stream<GroupModel> getGroupsStream() {
|
||||
if (updated != null) return updated.getGroupsStream();
|
||||
Set<GroupModel> groups = new LinkedHashSet<>();
|
||||
for (String id : cached.getGroups(modelSupplier)) {
|
||||
GroupModel groupModel = keycloakSession.groups().getGroupById(realm, id);
|
||||
if (groupModel == null) {
|
||||
// chance that role was removed, so just delete to persistence and get user invalidated
|
||||
getDelegateForUpdate();
|
||||
return updated.getGroupsStream();
|
||||
}
|
||||
groups.add(groupModel);
|
||||
Stream<GroupModel> result = Stream.empty();
|
||||
|
||||
if (updated != null) {
|
||||
result = updated.getGroupsStream();
|
||||
} else {
|
||||
Set<GroupModel> groups = null;
|
||||
for (String id : cached.getGroups(modelSupplier)) {
|
||||
GroupModel groupModel = keycloakSession.groups().getGroupById(realm, id);
|
||||
if (groupModel == null) {
|
||||
// chance that role was removed, so just delegate to persistence and get user invalidated
|
||||
getDelegateForUpdate();
|
||||
result = updated.getGroupsStream();
|
||||
break;
|
||||
} else {
|
||||
if (groups == null) {
|
||||
groups = new HashSet<>();
|
||||
}
|
||||
groups.add(groupModel);
|
||||
}
|
||||
}
|
||||
|
||||
if (groups != null) {
|
||||
result = groups.stream();
|
||||
}
|
||||
}
|
||||
return groups.stream();
|
||||
|
||||
return result.filter(g -> Type.REALM.equals(g.getType())).sorted(Comparator.comparing(GroupModel::getName));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan.entities;
|
|||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
|
||||
|
@ -42,6 +43,7 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
|||
private final LazyLoader<GroupModel, Set<String>> roleMappings;
|
||||
private final LazyLoader<GroupModel, Set<String>> subGroups;
|
||||
private final LazyLoader<GroupModel, Long> subGroupsCount;
|
||||
private final Type type;
|
||||
|
||||
public CachedGroup(Long revision, RealmModel realm, GroupModel group) {
|
||||
super(revision, group.getId());
|
||||
|
@ -52,6 +54,7 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
|||
this.roleMappings = new DefaultLazyLoader<>(source -> source.getRoleMappingsStream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet);
|
||||
this.subGroups = new DefaultLazyLoader<>(source -> source.getSubGroupsStream().map(GroupModel::getId).collect(Collectors.toSet()), Collections::emptySet);
|
||||
this.subGroupsCount = new DefaultLazyLoader<>(GroupModel::getSubGroupsCount, () -> 0L);
|
||||
this.type = group.getType();
|
||||
}
|
||||
|
||||
public String getRealm() {
|
||||
|
@ -85,4 +88,8 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
|||
public Long getSubGroupsCount(Supplier<GroupModel> group) {
|
||||
return subGroupsCount.get(group);
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2024 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.connections.jpa.updater.liquibase.custom;
|
||||
|
||||
import liquibase.exception.CustomChangeException;
|
||||
import liquibase.statement.core.RawSqlStatement;
|
||||
|
||||
public class JpaUpdate26_0_0_OrganizationGroupType extends CustomKeycloakTask {
|
||||
|
||||
@Override
|
||||
protected void generateStatementsImpl() throws CustomChangeException {
|
||||
String groupTable = getTableName("KEYCLOAK_GROUP");
|
||||
String orgTable = getTableName("ORG");
|
||||
|
||||
if ("mariadb".equals(database.getShortName())) {
|
||||
statements.add(new RawSqlStatement("UPDATE " + groupTable + " SET TYPE = 1 WHERE CONVERT(NAME USING utf8) IN (SELECT CONVERT(ID USING utf8) FROM " + orgTable + ")"));
|
||||
} else {
|
||||
statements.add(new RawSqlStatement("UPDATE " + groupTable + " SET TYPE = 1 WHERE NAME IN (SELECT ID FROM " + orgTable + ")"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTaskId() {
|
||||
return "Update type and id for organization groups";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +317,11 @@ public class GroupAdapter implements GroupModel , JpaModel<GroupEntity> {
|
|||
return getRoleMappingsStream().filter(r -> RoleUtils.isClientRole(r, app));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.valueOf(group.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -57,6 +57,7 @@ import org.keycloak.models.GroupModel;
|
|||
import org.keycloak.models.GroupModel.GroupCreatedEvent;
|
||||
import org.keycloak.models.GroupModel.GroupPathChangeEvent;
|
||||
import org.keycloak.models.GroupModel.GroupUpdatedEvent;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
|
@ -78,7 +79,6 @@ import org.keycloak.models.jpa.entities.RealmEntity;
|
|||
import org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity;
|
||||
import org.keycloak.models.jpa.entities.RoleEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
|
||||
|
||||
|
@ -180,20 +180,21 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
realm.getDefaultGroupIds().clear();
|
||||
em.flush();
|
||||
|
||||
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
|
||||
.setParameter("realm", realm.getId()).executeUpdate();
|
||||
|
||||
session.clients().removeClients(adapter);
|
||||
|
||||
num = em.createNamedQuery("deleteDefaultClientScopeRealmMappingByRealm")
|
||||
em.createNamedQuery("deleteDefaultClientScopeRealmMappingByRealm")
|
||||
.setParameter("realm", realm).executeUpdate();
|
||||
|
||||
session.clientScopes().removeClientScopes(adapter);
|
||||
session.roles().removeRoles(adapter);
|
||||
|
||||
session.groups().getTopLevelGroupsStream(adapter).forEach(Organizations.removeGroup(session, adapter));
|
||||
em.createNamedQuery("deleteOrganizationDomainsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteOrganizationsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
session.groups().preRemove(adapter);
|
||||
|
||||
num = em.createNamedQuery("removeClientInitialAccessByRealm")
|
||||
em.createNamedQuery("removeClientInitialAccessByRealm")
|
||||
.setParameter("realm", realm).executeUpdate();
|
||||
|
||||
em.remove(realm);
|
||||
|
@ -701,7 +702,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
}
|
||||
|
||||
@Override
|
||||
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
|
||||
public GroupModel createGroup(RealmModel realm, String id, Type type, String name, GroupModel toParent) {
|
||||
if (id == null) {
|
||||
id = KeycloakModelUtils.generateId();
|
||||
} else if (GroupEntity.TOP_PARENT_ID.equals(id)) {
|
||||
|
@ -713,6 +714,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
groupEntity.setName(name);
|
||||
groupEntity.setRealm(realm.getId());
|
||||
groupEntity.setParentId(toParent == null? GroupEntity.TOP_PARENT_ID : toParent.getId());
|
||||
groupEntity.setType(type == null ? Type.REALM.intValue() : type.intValue());
|
||||
em.persist(groupEntity);
|
||||
em.flush();
|
||||
|
||||
|
@ -739,6 +741,16 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
em.createNativeQuery("delete from " + clientScopeMapping + " where ROLE_ID = :role").setParameter("role", role.getId()).executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm) {
|
||||
em.createNamedQuery("deleteGroupRoleMappingsByRealm")
|
||||
.setParameter("realm", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteGroupAttributesByRealm")
|
||||
.setParameter("realm", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteGroupsByRealm")
|
||||
.setParameter("realm", realm.getId()).executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel addClient(RealmModel realm, String clientId) {
|
||||
return addClient(realm, KeycloakModelUtils.generateId(), clientId);
|
||||
|
@ -1164,6 +1176,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
|||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
predicates.add(builder.equal(root.get("realm"), realm.getId()));
|
||||
predicates.add(builder.equal(root.get("type"), Type.REALM.intValue()));
|
||||
|
||||
for (Map.Entry<String, String> entry : filteredAttributes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.credential.UserCredentialManager;
|
||||
|
@ -45,6 +47,8 @@ import jakarta.persistence.criteria.CriteriaBuilder;
|
|||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -391,7 +395,15 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
|||
|
||||
@Override
|
||||
public long getGroupsCount() {
|
||||
return createCountGroupsQuery().getSingleResult();
|
||||
Long result = createCountGroupsQuery().getSingleResult();
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
if (result > 0 && provider.getByMember(this) != null) {
|
||||
// remove from the count the organization group membership
|
||||
result--;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -37,6 +37,7 @@ import jakarta.persistence.Table;
|
|||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="getGroupAttributesByNameAndValue", query="select attr from GroupAttributeEntity attr where attr.name = :name and attr.value = :value"),
|
||||
@NamedQuery(name="deleteGroupAttributesByRealm", query="delete from GroupAttributeEntity a where a.group IN (select u from GroupEntity u where u.realm=:realm)")
|
||||
})
|
||||
@Table(name="GROUP_ATTRIBUTE")
|
||||
@Entity
|
||||
|
|
|
@ -28,17 +28,18 @@ import java.util.LinkedList;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.realm = :realm and u.parentId = :parent order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByParentAndName", query="select u.id from GroupEntity u where u.realm = :realm and u.parentId = :parent and u.name = :search order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByParentAndNameContaining", query="select u.id from GroupEntity u where u.realm = :realm and u.parentId = :parent and lower(u.name) like lower(concat('%',:search,'%')) order by u.name ASC"),
|
||||
@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 lower(u.name) like lower(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="getGroupIdsByName", query="select u.id from GroupEntity u where u.realm = :realm and u.name = :search 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="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"),
|
||||
@NamedQuery(name="getGroupCountByParent", query="select count(u) from GroupEntity u where u.realm = :realm and u.parentId = :parent")
|
||||
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and u.parentId = :parent order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByParentAndName", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and u.parentId = :parent and u.name = :search order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByParentAndNameContaining", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and u.parentId = :parent and lower(u.name) like lower(concat('%',:search,'%')) order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByRealm", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and lower(u.name) like lower(concat('%',:search,'%')) order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByNameContainingFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsByName", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 and u.name = :search order by u.name ASC"),
|
||||
@NamedQuery(name="getGroupIdsFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and u.type = 0 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 u.type = 0 and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids"),
|
||||
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm and u.type = 0"),
|
||||
@NamedQuery(name="getGroupCountByParent", query="select count(u) from GroupEntity u where u.realm = :realm and u.type = 0 and u.parentId = :parent"),
|
||||
@NamedQuery(name="deleteGroupsByRealm", query="delete from GroupEntity g where g.realm = :realm")
|
||||
})
|
||||
@Entity
|
||||
@Table(name="KEYCLOAK_GROUP",
|
||||
|
@ -66,6 +67,9 @@ public class GroupEntity {
|
|||
@Column(name = "REALM_ID")
|
||||
private String realm;
|
||||
|
||||
@Column(name = "TYPE")
|
||||
private int type;
|
||||
|
||||
@OneToMany(
|
||||
cascade = CascadeType.REMOVE,
|
||||
orphanRemoval = true, mappedBy="group")
|
||||
|
@ -114,6 +118,14 @@ public class GroupEntity {
|
|||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -25,6 +25,8 @@ import jakarta.persistence.FetchType;
|
|||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.NamedQueries;
|
||||
import jakarta.persistence.NamedQuery;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
|
@ -34,6 +36,9 @@ import jakarta.persistence.Table;
|
|||
*/
|
||||
@Entity
|
||||
@Table(name="ORG_DOMAIN")
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="deleteOrganizationDomainsByRealm", query="delete from OrganizationDomainEntity d where d.organization IN (select o from OrganizationEntity o where o.realmId=:realmId)")
|
||||
})
|
||||
public class OrganizationDomainEntity {
|
||||
|
||||
@Id
|
||||
|
|
|
@ -43,7 +43,8 @@ import jakarta.persistence.Table;
|
|||
" where o.realmId = :realmId AND (o.name = :search OR d.name = :search) order by o.name ASC"),
|
||||
@NamedQuery(name="getByNameOrDomainContained", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
|
||||
" where o.realmId = :realmId AND (lower(o.name) like concat('%',:search,'%') OR d.name like concat('%',:search,'%')) order by o.name ASC"),
|
||||
@NamedQuery(name="getCount", query="select count(o) from OrganizationEntity o where o.realmId = :realmId")
|
||||
@NamedQuery(name="getCount", query="select count(o) from OrganizationEntity o where o.realmId = :realmId"),
|
||||
@NamedQuery(name="deleteOrganizationsByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId")
|
||||
})
|
||||
public class OrganizationEntity {
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import jakarta.persistence.criteria.Root;
|
|||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -451,11 +452,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
}
|
||||
|
||||
private GroupModel createOrganizationGroup(String orgId) {
|
||||
GroupModel group = groupProvider.createGroup(getRealm(), null, orgId);
|
||||
|
||||
group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId);
|
||||
|
||||
return group;
|
||||
return groupProvider.createGroup(getRealm(), null, Type.ORGANIZATION, orgId, null);
|
||||
}
|
||||
|
||||
private GroupModel getOrganizationGroup(OrganizationModel organization) {
|
||||
|
|
|
@ -38,6 +38,8 @@ import org.keycloak.provider.ProviderEvent;
|
|||
|
||||
public class JpaOrganizationProviderFactory implements OrganizationProviderFactory {
|
||||
|
||||
public static final String ID = "jpa";
|
||||
|
||||
@Override
|
||||
public OrganizationProvider create(KeycloakSession session) {
|
||||
return new JpaOrganizationProvider(session);
|
||||
|
@ -60,7 +62,7 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto
|
|||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "jpa";
|
||||
return ID;
|
||||
}
|
||||
|
||||
private void handleEvents(ProviderEvent event) {
|
||||
|
@ -68,11 +70,6 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto
|
|||
RealmModel realm = ((RealmPostCreateEvent) event).getCreatedRealm();
|
||||
configureAuthenticationFlows(realm);
|
||||
}
|
||||
if (event instanceof RealmRemovedEvent) {
|
||||
KeycloakSession session = ((RealmRemovedEvent) event).getKeycloakSession();
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
provider.removeAll();
|
||||
}
|
||||
if (event instanceof GroupEvent) {
|
||||
GroupEvent groupEvent = (GroupEvent) event;
|
||||
KeycloakSession session = groupEvent.getKeycloakSession();
|
||||
|
|
|
@ -75,7 +75,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
return realm;
|
||||
}
|
||||
|
||||
String getGroupId() {
|
||||
public String getGroupId() {
|
||||
return entity.getGroupId();
|
||||
}
|
||||
|
||||
|
@ -137,8 +137,6 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
// make sure the kc.org attribute is never removed or updated
|
||||
attributes.put(ORGANIZATION_ATTRIBUTE, getGroup().getAttributes().get(OrganizationModel.ORGANIZATION_ATTRIBUTE));
|
||||
Set<String> attrsToRemove = getAttributes().keySet();
|
||||
attrsToRemove.removeAll(attributes.keySet());
|
||||
attrsToRemove.forEach(group::removeAttribute);
|
||||
|
@ -148,9 +146,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
@Override
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
if (attributes == null) {
|
||||
attributes = new HashMap<>(ofNullable(getGroup().getAttributes()).orElse(Map.of()));
|
||||
// do not expose the kc.org attribute
|
||||
attributes.remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
attributes = ofNullable(getGroup().getAttributes()).orElse(Map.of());
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
|
|
@ -28,4 +28,21 @@
|
|||
<addUniqueConstraint tableName="ORG" columnNames="REALM_ID, ALIAS" constraintName="UK_ORG_ALIAS"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="keycloak" id="26.0.0-org-group">
|
||||
<addColumn tableName="KEYCLOAK_GROUP">
|
||||
<column name="TYPE" type="INT" defaultValueNumeric="0"/>
|
||||
</addColumn>
|
||||
<update tableName="KEYCLOAK_GROUP">
|
||||
<column name="TYPE" valueNumeric="0" type="INT"/>
|
||||
</update>
|
||||
<addNotNullConstraint tableName="KEYCLOAK_GROUP" columnName="TYPE" columnDataType="INT"/>
|
||||
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate26_0_0_OrganizationGroupType"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="keycloak" id="26.0.0-org-indexes">
|
||||
<createIndex tableName="ORG_DOMAIN" indexName="IDX_ORG_DOMAIN_ORG_ID">
|
||||
<column name="ORG_ID" type="VARCHAR(255)"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -31,6 +31,8 @@ import org.keycloak.exportimport.ExportOptions;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -458,7 +460,7 @@ public class ExportUtils {
|
|||
|
||||
if (options.isGroupsAndRolesIncluded()) {
|
||||
List<String> groups = user.getGroupsStream()
|
||||
.filter(g -> !g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE))
|
||||
.filter(g -> Type.REALM.equals(g.getType()))
|
||||
.map(ModelToRepresentation::buildGroupPath).collect(Collectors.toList());
|
||||
userRep.setGroups(groups);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.storage;
|
|||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -119,8 +120,8 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
|
|||
}
|
||||
|
||||
@Override
|
||||
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
|
||||
return localStorage().createGroup(realm, id, name, toParent);
|
||||
public GroupModel createGroup(RealmModel realm, String id, Type type, String name, GroupModel toParent) {
|
||||
return localStorage().createGroup(realm, id, type, name, toParent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -138,6 +139,11 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
|
|||
localStorage().addTopLevelGroup(realm, subGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm) {
|
||||
localStorage().preRemove(realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -172,7 +172,6 @@ public class ModelToRepresentation {
|
|||
@Deprecated
|
||||
public static Stream<GroupRepresentation> toGroupHierarchy(KeycloakSession session, RealmModel realm, boolean full) {
|
||||
return session.groups().getTopLevelGroupsStream(realm, null, null)
|
||||
.filter(g -> !g.getAttributes().containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE))
|
||||
.map(g -> toGroupHierarchy(g, full));
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,33 @@ import java.util.stream.Stream;
|
|||
*/
|
||||
public interface GroupModel extends RoleMapperModel {
|
||||
|
||||
enum Type {
|
||||
REALM(0),
|
||||
ORGANIZATION(1);
|
||||
|
||||
private final int value;
|
||||
|
||||
Type(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static Type valueOf(int value) {
|
||||
Type[] values = values();
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].value == value) {
|
||||
return values[i];
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No type found with value " + value);
|
||||
}
|
||||
|
||||
public int intValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
interface GroupEvent extends ProviderEvent {
|
||||
RealmModel getRealm();
|
||||
GroupModel getGroup();
|
||||
|
@ -292,4 +319,8 @@ public interface GroupModel extends RoleMapperModel {
|
|||
default boolean escapeSlashesInGroupPath() {
|
||||
return GroupProvider.DEFAULT_ESCAPE_SLASHES;
|
||||
}
|
||||
|
||||
default Type getType() {
|
||||
return Type.REALM;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.storage.group.GroupLookupProvider;
|
||||
|
||||
|
@ -203,7 +204,22 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @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);
|
||||
default GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
|
||||
return createGroup(realm, id, Type.REALM, name, toParent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group with the given name, id, name and parent to the given realm.
|
||||
*
|
||||
* @param realm Realm.
|
||||
* @param id Id, will be generated if {@code null}.
|
||||
* @param type the group type. if not set, defaults to {@link Type#REALM}
|
||||
* @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, Type type, String name, GroupModel toParent);
|
||||
|
||||
/**
|
||||
* Removes the given group for the given realm.
|
||||
|
@ -237,4 +253,12 @@ public interface GroupProvider extends Provider, GroupLookupProvider {
|
|||
* @throws ModelDuplicateException If there is already a top level group name with the same name
|
||||
*/
|
||||
void addTopLevelGroup(RealmModel realm, GroupModel subGroup);
|
||||
|
||||
/**
|
||||
* Called when a realm is removed.
|
||||
* Should remove all groups that belong to the realm.
|
||||
*
|
||||
* @param realm a reference to the realm
|
||||
*/
|
||||
void preRemove(RealmModel realm);
|
||||
}
|
||||
|
|
|
@ -134,9 +134,9 @@ public class OrganizationsResource {
|
|||
// check if are searching orgs by attribute.
|
||||
if (StringUtil.isNotBlank(searchQuery)) {
|
||||
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
|
||||
return provider.getAllStream(attributes, first, max).map(Organizations::toRepresentation);
|
||||
return provider.getAllStream(attributes, first, max).map(Organizations::toBriefRepresentation);
|
||||
} else {
|
||||
return provider.getAllStream(search, exact, first, max).map(Organizations::toRepresentation);
|
||||
return provider.getAllStream(search, exact, first, max).map(Organizations::toBriefRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.keycloak.http.HttpRequest;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationDomainModel;
|
||||
|
@ -51,16 +52,14 @@ import org.keycloak.utils.StringUtil;
|
|||
public class Organizations {
|
||||
|
||||
public static boolean canManageOrganizationGroup(KeycloakSession session, GroupModel group) {
|
||||
if (!Type.ORGANIZATION.equals(group.getType())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
Object organization = session.getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
|
||||
if (organization != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String orgId = group.getFirstAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
|
||||
return StringUtil.isBlank(orgId);
|
||||
return organization != null && organization.getId().equals(group.getName());
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -101,7 +100,7 @@ public class Organizations {
|
|||
|
||||
public static Consumer<GroupModel> removeGroup(KeycloakSession session, RealmModel realm) {
|
||||
return group -> {
|
||||
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
if (!Type.ORGANIZATION.equals(group.getType())) {
|
||||
realm.removeGroup(group);
|
||||
return;
|
||||
}
|
||||
|
@ -109,12 +108,9 @@ public class Organizations {
|
|||
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
|
||||
try {
|
||||
String orgId = group.getFirstAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
|
||||
if (orgId != null) {
|
||||
session.setAttribute(OrganizationModel.class.getName(), provider.getById(orgId));
|
||||
}
|
||||
session.setAttribute(OrganizationModel.class.getName(), provider.getById(group.getName()));
|
||||
|
||||
realm.removeGroup(group);
|
||||
} finally {
|
||||
|
@ -138,6 +134,18 @@ public class Organizations {
|
|||
}
|
||||
|
||||
public static OrganizationRepresentation toRepresentation(OrganizationModel model) {
|
||||
OrganizationRepresentation rep = toBriefRepresentation(model);
|
||||
|
||||
if (rep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
rep.setAttributes(model.getAttributes());
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
public static OrganizationRepresentation toBriefRepresentation(OrganizationModel model) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -149,7 +157,6 @@ public class Organizations {
|
|||
rep.setAlias(model.getAlias());
|
||||
rep.setEnabled(model.isEnabled());
|
||||
rep.setDescription(model.getDescription());
|
||||
rep.setAttributes(model.getAttributes());
|
||||
model.getDomains().filter(Objects::nonNull).map(Organizations::toRepresentation)
|
||||
.forEach(rep::addDomain);
|
||||
|
||||
|
|
|
@ -19,19 +19,16 @@ package org.keycloak.testsuite.organization.admin;
|
|||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.admin.group.GroupSearchTest.buildSearchQuery;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.GroupResource;
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.ModelValidationException;
|
||||
|
@ -39,8 +36,9 @@ import org.keycloak.models.OrganizationModel;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.organization.jpa.JpaOrganizationProviderFactory;
|
||||
import org.keycloak.organization.jpa.OrganizationAdapter;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
|
@ -49,167 +47,39 @@ import org.keycloak.testsuite.runonserver.RunOnServer;
|
|||
@EnableFeature(Feature.ORGANIZATION)
|
||||
public class OrganizationGroupTest extends AbstractOrganizationTest {
|
||||
|
||||
@Test
|
||||
public void testManageOrgGroupsViaDifferentAPIs() {
|
||||
// test realm contains some groups initially
|
||||
List<GroupRepresentation> getAllBefore = testRealm().groups().groups();
|
||||
long countBefore = testRealm().groups().count().get("count");
|
||||
|
||||
List<String> orgIds = new ArrayList<>();
|
||||
// create 5 organizations
|
||||
for (int i = 0; i < 5; i++) {
|
||||
OrganizationRepresentation expected = createOrganization("myorg" + i);
|
||||
OrganizationResource organization = testRealm().organizations().get(expected.getId());
|
||||
expected.setAttributes(Map.of());
|
||||
organization.update(expected).close();
|
||||
OrganizationRepresentation existing = organization.toRepresentation();
|
||||
orgIds.add(expected.getId());
|
||||
assertNotNull(existing);
|
||||
}
|
||||
|
||||
// create one top-level group and one subgroup
|
||||
GroupRepresentation topGroup = createGroup(testRealm(), "top");
|
||||
GroupRepresentation level2Group = new GroupRepresentation();
|
||||
level2Group.setName("level2");
|
||||
testRealm().groups().group(topGroup.getId()).subGroup(level2Group);
|
||||
|
||||
// check that count queries include org related groups
|
||||
assertEquals(countBefore + 7, (long) testRealm().groups().count().get("count"));
|
||||
|
||||
// check that search queries include org related groups but those can't be updated
|
||||
assertEquals(getAllBefore.size() + 6, testRealm().groups().groups().size());
|
||||
// we need to pull full representation of the group, otherwise org related attributes are lost in the representation
|
||||
List<GroupRepresentation> groups = testRealm().groups().query(buildSearchQuery(OrganizationModel.ORGANIZATION_ATTRIBUTE, orgIds.get(0)), false, 0, 10, false);
|
||||
assertEquals(1, groups.size());
|
||||
GroupRepresentation orgGroupRep = groups.get(0);
|
||||
GroupResource group = testRealm().groups().group(orgGroupRep.getId());
|
||||
|
||||
try {
|
||||
// group to be updated is organization related group
|
||||
group.update(topGroup);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success, the group could not be updated
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot update a group with the attribute reserved for organization related groups
|
||||
testRealm().groups().group(topGroup.getId()).update(orgGroupRep);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success, the group could not be updated
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot remove organization related group
|
||||
group.remove();
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success, the group could not be removed
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot manage organization related group permissions
|
||||
group.setPermissions(new ManagementPermissionRepresentation(true));
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success, the group's permissions cannot be managed
|
||||
}
|
||||
|
||||
// try to add subgroup to an org related group
|
||||
try (Response response = group.subGroup(topGroup)) {
|
||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||
}
|
||||
|
||||
// try to add org related group as a subgroup to a group
|
||||
try (Response response = testRealm().groups().group(topGroup.getId()).subGroup(orgGroupRep)) {
|
||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot manage organization related group role mappers
|
||||
group.roles().realmLevel().add(null);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot manage organization related group role mappers
|
||||
group.roles().realmLevel().remove(null);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot manage organization related group role mappers
|
||||
group.roles().clientLevel(testRealm().clients().findByClientId("test-app").get(0).getId()).add(null);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot manage organization related group role mappers
|
||||
group.roles().clientLevel(testRealm().clients().findByClientId("test-app").get(0).getId()).remove(null);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
|
||||
// cannot add top level group with reserved attribute for organizations
|
||||
String id = orgGroupRep.getId();
|
||||
String name = orgGroupRep.getName();
|
||||
orgGroupRep.setId(null);
|
||||
orgGroupRep.setName(null);
|
||||
try (Response response = testRealm().groups().add(orgGroupRep)) {
|
||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||
} finally {
|
||||
orgGroupRep.setId(id);
|
||||
orgGroupRep.setName(name);
|
||||
}
|
||||
|
||||
try {
|
||||
// cannot add organization related group as a default group
|
||||
testRealm().addDefaultGroup(orgGroupRep.getId());
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
|
||||
OrganizationRepresentation org = createOrganization();
|
||||
UserRepresentation userRep = addMember(testRealm().organizations().get(org.getId()));
|
||||
|
||||
try {
|
||||
// cannot join organization related group
|
||||
testRealm().users().get(userRep.getId()).joinGroup(orgGroupRep.getId());
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException ex) {
|
||||
// success
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManagingOrganizationGroupNotInOrganizationScope() {
|
||||
String id = createOrganization().getId();
|
||||
String memberId = addMember(testRealm().organizations().get(id)).getId();
|
||||
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) session -> {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
OrganizationModel organization = provider.getById(id);
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class, JpaOrganizationProviderFactory.ID);
|
||||
OrganizationAdapter organization = (OrganizationAdapter) provider.getById(id);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
GroupModel orgGroup = session.groups().getGroupByName(realm, null, organization.getId());
|
||||
GroupModel orgGroup = session.groups().getGroupById(realm, organization.getGroupId());
|
||||
assertNotNull(orgGroup);
|
||||
|
||||
try {
|
||||
orgGroup.setName("fail");
|
||||
fail("can not manage");
|
||||
} catch (ModelValidationException ignore) {
|
||||
try {
|
||||
orgGroup.setName(organization.getId());
|
||||
} catch (ModelValidationException ignore2) {}
|
||||
}
|
||||
|
||||
try {
|
||||
orgGroup.setSingleAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE, "fail");
|
||||
fail("can not manage");
|
||||
} catch (ModelValidationException ignore) {
|
||||
try {
|
||||
orgGroup.setSingleAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId());
|
||||
} catch (ModelValidationException ignore2) {}
|
||||
}
|
||||
|
||||
try {
|
||||
orgGroup.setSingleAttribute("something", "fail");
|
||||
fail("can not manage");
|
||||
} catch (ModelValidationException ignore) {
|
||||
}
|
||||
|
||||
|
@ -259,6 +129,44 @@ public class OrganizationGroupTest extends AbstractOrganizationTest {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrganizationGroupsNotAvailableFromGroupAPI() {
|
||||
Set<String> orgIds = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
orgIds.add(createOrganization("org-" + i).getId());
|
||||
}
|
||||
|
||||
assertEquals(orgIds.size(), testRealm().organizations().getAll().size());
|
||||
assertTrue(testRealm().groups().groups().stream().map(GroupRepresentation::getId).noneMatch(orgIds::contains));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrganizationGroupsNotAvailableFromUserAPI() {
|
||||
OrganizationRepresentation organization = createOrganization();
|
||||
UserRepresentation member = addMember(testRealm().organizations().get(organization.getId()));
|
||||
UserResource userResource = testRealm().users().get(member.getId());
|
||||
assertTrue(userResource.groups().isEmpty());
|
||||
assertEquals(0, userResource.groupsCount(null).get("count").intValue());
|
||||
assertEquals(0, userResource.groupsCount(organization.getId()).get("count").intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteGroupOnOrganizationRemoval() {
|
||||
String id = createOrganization().getId();
|
||||
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) session -> {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class, JpaOrganizationProviderFactory.ID);
|
||||
OrganizationAdapter organization = (OrganizationAdapter) provider.getById(id);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
GroupModel group = session.groups().getGroupById(realm, organization.getGroupId());
|
||||
assertNotNull(group);
|
||||
provider.remove(organization);
|
||||
group = session.groups().getGroupById(realm, organization.getId());
|
||||
assertNull(group);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OrganizationRepresentation createRepresentation(String name, String... orgDomains) {
|
||||
OrganizationRepresentation rep = super.createRepresentation(name, orgDomains);
|
||||
|
|
|
@ -36,6 +36,8 @@ import java.util.ArrayList;
|
|||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
|
@ -43,6 +45,8 @@ import jakarta.ws.rs.core.Response;
|
|||
import jakarta.ws.rs.core.Response.Status;
|
||||
import java.io.IOException;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.admin.client.resource.OrganizationsResource;
|
||||
|
@ -104,12 +108,16 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
|||
List<OrganizationRepresentation> expected = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
expected.add(createOrganization("kc.org." + i));
|
||||
OrganizationRepresentation organization = createOrganization("kc.org." + i);
|
||||
expected.add(organization);
|
||||
organization.setAttributes(Map.of("foo", List.of("foo")));
|
||||
testRealm().organizations().get(organization.getId()).update(organization).close();
|
||||
}
|
||||
|
||||
List<OrganizationRepresentation> existing = testRealm().organizations().getAll();
|
||||
assertFalse(existing.isEmpty());
|
||||
assertThat(expected, containsInAnyOrder(existing.toArray()));
|
||||
Assert.assertTrue(existing.stream().map(OrganizationRepresentation::getAttributes).filter(Objects::nonNull).findAny().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -129,6 +137,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
|||
assertThat(orgRep.getDomains(), hasSize(2));
|
||||
assertThat(orgRep.getDomain("wayneind.com"), not(nullValue()));
|
||||
assertThat(orgRep.getDomain("wayneind-gotham.com"), not(nullValue()));
|
||||
assertThat(orgRep.getAttributes(), nullValue());
|
||||
|
||||
existing = testRealm().organizations().search("gtbank.net", true, 0, 10);
|
||||
assertThat(existing, hasSize(1));
|
||||
|
@ -138,6 +147,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
|||
assertThat(orgRep.getDomains(), hasSize(2));
|
||||
assertThat(orgRep.getDomain("gtbank.com"), not(nullValue()));
|
||||
assertThat(orgRep.getDomain("gtbank.net"), not(nullValue()));
|
||||
assertThat(orgRep.getAttributes(), nullValue());
|
||||
|
||||
existing = testRealm().organizations().search("nonexistent.org", true, 0, 10);
|
||||
assertThat(existing, is(empty()));
|
||||
|
|
|
@ -163,7 +163,10 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
|||
exportImport.setAction(ExportImportConfig.ACTION_IMPORT);
|
||||
exportImport.setFile(targetFilePath);
|
||||
exportImport.runImport();
|
||||
getCleanup().addCleanup(() -> testRealm().remove());
|
||||
getCleanup().addCleanup(() -> {
|
||||
testRealm().remove();
|
||||
getTestContext().getTestRealmReps().clear();
|
||||
});
|
||||
|
||||
return testRealm().toRepresentation();
|
||||
}
|
||||
|
|
|
@ -21,14 +21,24 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||
|
@ -55,4 +65,35 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
|||
assertThat(claim, notNullValue());
|
||||
assertThat(claim.get(organizationName), notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrganizationNotAddedByGroupMapper() throws Exception {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
addMember(organization);
|
||||
ClientRepresentation client = testRealm().clients().findByClientId("direct-grant").get(0);
|
||||
ClientResource clientResource = testRealm().clients().get(client.getId());
|
||||
clientResource.getProtocolMappers().createMapper(createGroupMapper()).close();
|
||||
|
||||
oauth.clientId("direct-grant");
|
||||
oauth.scope("openid organization");
|
||||
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword);
|
||||
assertThat(response.getScope(), containsString("organization"));
|
||||
AccessToken accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken();
|
||||
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||
assertThat(accessToken.getOtherClaims().get("groups"), nullValue());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ProtocolMapperRepresentation createGroupMapper() {
|
||||
ProtocolMapperRepresentation groupMapper = new ProtocolMapperRepresentation();
|
||||
groupMapper.setName("groups");
|
||||
groupMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
|
||||
groupMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
Map<String, String> config = new HashMap<>();
|
||||
config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups.groups");
|
||||
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
||||
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
|
||||
groupMapper.setConfig(config);
|
||||
return groupMapper;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -345,18 +345,6 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteGroupOnOrganizationRemoval() {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
addMember(organization);
|
||||
|
||||
assertTrue(testRealm().groups().groups("", 0, 100, false).stream().anyMatch(group -> group.getAttributes().containsKey("kc.org")));
|
||||
|
||||
organization.delete().close();
|
||||
|
||||
assertFalse(testRealm().groups().groups("", 0, 100, false).stream().anyMatch(group -> group.getAttributes().containsKey("kc.org")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchMembers() {
|
||||
|
||||
|
|
Loading…
Reference in a new issue