Prevent to manage groups associated with organizations from different APIs
Closes #28734 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
parent
f8bc74d64f
commit
d4b7e1a7d9
11 changed files with 296 additions and 24 deletions
|
@ -36,6 +36,7 @@ import jakarta.persistence.Table;
|
||||||
@Entity
|
@Entity
|
||||||
@NamedQueries({
|
@NamedQueries({
|
||||||
@NamedQuery(name="getByRealm", query="select o from OrganizationEntity o where o.realmId = :realmId order by o.name ASC"),
|
@NamedQuery(name="getByRealm", query="select o from OrganizationEntity o where o.realmId = :realmId order by o.name ASC"),
|
||||||
|
@NamedQuery(name="getByOrgName", query="select distinct o from OrganizationEntity o where o.realmId = :realmId AND o.name = :name"),
|
||||||
@NamedQuery(name="getByNameOrDomain", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
|
@NamedQuery(name="getByNameOrDomain", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
|
||||||
" where o.realmId = :realmId AND (o.name = :search OR d.name = :search) order by o.name ASC"),
|
" 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" +
|
@NamedQuery(name="getByNameOrDomainContained", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
|
||||||
|
|
|
@ -23,7 +23,6 @@ import static org.keycloak.utils.StreamsUtil.closing;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -73,10 +72,15 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
throw new ModelValidationException("Name can not be null");
|
throw new ModelValidationException("Name can not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupModel group = createOrganizationGroup(name);
|
if (getByName(name) != null) {
|
||||||
OrganizationEntity entity = new OrganizationEntity();
|
throw new ModelDuplicateException("A organization with the same name already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationEntity entity = new OrganizationEntity();
|
||||||
entity.setId(KeycloakModelUtils.generateId());
|
entity.setId(KeycloakModelUtils.generateId());
|
||||||
|
|
||||||
|
GroupModel group = createOrganizationGroup(entity.getId());
|
||||||
|
|
||||||
entity.setGroupId(group.getId());
|
entity.setGroupId(group.getId());
|
||||||
entity.setRealmId(realm.getId());
|
entity.setRealmId(realm.getId());
|
||||||
entity.setName(name);
|
entity.setName(name);
|
||||||
|
@ -335,21 +339,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupModel createOrganizationGroup(String name) {
|
private GroupModel createOrganizationGroup(String orgId) {
|
||||||
throwExceptionIfObjectIsNull(name, "Name of the group");
|
GroupModel group = groupProvider.createGroup(realm, null, orgId);
|
||||||
|
group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId);
|
||||||
|
|
||||||
String groupName = getCanonicalGroupName(name);
|
return group;
|
||||||
GroupModel group = groupProvider.getGroupByName(realm, null, name);
|
|
||||||
|
|
||||||
if (group != null) {
|
|
||||||
throw new ModelDuplicateException("A group with the same name already exist and it is bound to different organization");
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupProvider.createGroup(realm, groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCanonicalGroupName(String name) {
|
|
||||||
return "kc.org." + name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupModel getOrganizationGroup(OrganizationModel organization) {
|
private GroupModel getOrganizationGroup(OrganizationModel organization) {
|
||||||
|
@ -370,4 +364,17 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
throw new ModelException(String.format("%s cannot be null", objectName));
|
throw new ModelException(String.format("%s cannot be null", objectName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private OrganizationEntity getByName(String name) {
|
||||||
|
TypedQuery<OrganizationEntity> query = em.createNamedQuery("getByOrgName", OrganizationEntity.class);
|
||||||
|
|
||||||
|
query.setParameter("name", name);
|
||||||
|
query.setParameter("realmId", realm.getId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return query.getSingleResult();
|
||||||
|
} catch (NoResultException nre) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.keycloak.utils.GroupUtils;
|
import org.keycloak.utils.GroupUtils;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupRep;
|
||||||
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -121,6 +122,8 @@ public class GroupResource {
|
||||||
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupRep(session, rep);
|
||||||
|
|
||||||
if (!Objects.equals(groupName, group.getName())) {
|
if (!Objects.equals(groupName, group.getName())) {
|
||||||
boolean exists = siblings().filter(s -> !Objects.equals(s.getId(), group.getId()))
|
boolean exists = siblings().filter(s -> !Objects.equals(s.getId(), group.getId()))
|
||||||
.anyMatch(s -> Objects.equals(s.getName(), groupName));
|
.anyMatch(s -> Objects.equals(s.getName(), groupName));
|
||||||
|
@ -194,6 +197,8 @@ public class GroupResource {
|
||||||
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupRep(session, rep);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Response.ResponseBuilder builder = Response.status(204);
|
Response.ResponseBuilder builder = Response.status(204);
|
||||||
GroupModel child = null;
|
GroupModel child = null;
|
||||||
|
@ -367,6 +372,7 @@ public class GroupResource {
|
||||||
@Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference")
|
@Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference")
|
||||||
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
|
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
|
||||||
auth.groups().requireManage(group);
|
auth.groups().requireManage(group);
|
||||||
|
|
||||||
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
|
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
|
||||||
permissions.groups().setPermissionsEnabled(group, ref.isEnabled());
|
permissions.groups().setPermissionsEnabled(group, ref.isEnabled());
|
||||||
if (ref.isEnabled()) {
|
if (ref.isEnabled()) {
|
||||||
|
@ -375,6 +381,5 @@ public class GroupResource {
|
||||||
return new ManagementPermissionReference();
|
return new ManagementPermissionReference();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluato
|
||||||
import org.keycloak.utils.GroupUtils;
|
import org.keycloak.utils.GroupUtils;
|
||||||
import org.keycloak.utils.SearchQueryUtils;
|
import org.keycloak.utils.SearchQueryUtils;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupModel;
|
||||||
|
import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupRep;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,6 +127,9 @@ public class GroupsResource {
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new NotFoundException("Could not find group by id");
|
throw new NotFoundException("Could not find group by id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupModel(session, group);
|
||||||
|
|
||||||
return new GroupResource(realm, group, session, this.auth, adminEvent);
|
return new GroupResource(realm, group, session, this.auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +181,8 @@ public class GroupsResource {
|
||||||
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupRep(session, rep);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (rep.getId() != null) {
|
if (rep.getId() != null) {
|
||||||
child = realm.getGroupById(rep.getId());
|
child = realm.getGroupById(rep.getId());
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.keycloak.services.resources.admin;
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
import static org.keycloak.util.JsonSerialization.readValue;
|
import static org.keycloak.util.JsonSerialization.readValue;
|
||||||
|
import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupModel;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
@ -30,7 +31,6 @@ import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import jakarta.enterprise.inject.Default;
|
|
||||||
import jakarta.ws.rs.DefaultValue;
|
import jakarta.ws.rs.DefaultValue;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
|
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
|
||||||
|
@ -49,7 +49,6 @@ import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.PathSegment;
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.core.StreamingOutput;
|
import jakarta.ws.rs.core.StreamingOutput;
|
||||||
|
@ -94,7 +93,6 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.models.utils.StripSecretsUtils;
|
|
||||||
import org.keycloak.partialimport.ErrorResponseException;
|
import org.keycloak.partialimport.ErrorResponseException;
|
||||||
import org.keycloak.partialimport.PartialImportResult;
|
import org.keycloak.partialimport.PartialImportResult;
|
||||||
import org.keycloak.partialimport.PartialImportResults;
|
import org.keycloak.partialimport.PartialImportResults;
|
||||||
|
@ -1053,6 +1051,9 @@ public class RealmAdminResource {
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new NotFoundException("Group not found");
|
throw new NotFoundException("Group not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupModel(session, group);
|
||||||
|
|
||||||
realm.addDefaultGroup(group);
|
realm.addDefaultGroup(group);
|
||||||
|
|
||||||
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
|
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
|
||||||
|
@ -1070,6 +1071,9 @@ public class RealmAdminResource {
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new NotFoundException("Group not found");
|
throw new NotFoundException("Group not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupModel(session, group);
|
||||||
|
|
||||||
realm.removeDefaultGroup(group);
|
realm.removeDefaultGroup(group);
|
||||||
|
|
||||||
adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
|
adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
|
||||||
|
|
|
@ -128,6 +128,7 @@ import java.util.stream.Stream;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base resource for managing users
|
* Base resource for managing users
|
||||||
|
@ -1017,6 +1018,8 @@ public class UserResource {
|
||||||
}
|
}
|
||||||
auth.groups().requireManageMembership(group);
|
auth.groups().requireManageMembership(group);
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupModel(session, group);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (user.isMemberOf(group)){
|
if (user.isMemberOf(group)){
|
||||||
user.leaveGroup(group);
|
user.leaveGroup(group);
|
||||||
|
@ -1044,6 +1047,9 @@ public class UserResource {
|
||||||
throw new NotFoundException("Group not found");
|
throw new NotFoundException("Group not found");
|
||||||
}
|
}
|
||||||
auth.groups().requireManageMembership(group);
|
auth.groups().requireManageMembership(group);
|
||||||
|
|
||||||
|
checkForOrgRelatedGroupModel(session, group);
|
||||||
|
|
||||||
if (!RoleUtils.isDirectMember(user.getGroupsStream(),group)){
|
if (!RoleUtils.isDirectMember(user.getGroupsStream(),group)){
|
||||||
user.joinGroup(group);
|
user.joinGroup(group);
|
||||||
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(session.getContext().getUri()).success();
|
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(session.getContext().getUri()).success();
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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.utils;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OrganizationUtils {
|
||||||
|
|
||||||
|
public static void checkForOrgRelatedGroupRep(KeycloakSession session, GroupRepresentation rep) {
|
||||||
|
if (isOrgsEnabled(session)) {
|
||||||
|
checkRep(rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkForOrgRelatedGroupModel(KeycloakSession session, GroupModel model) {
|
||||||
|
if (isOrgsEnabled(session)) {
|
||||||
|
checkModel(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOrgsEnabled(KeycloakSession session) {
|
||||||
|
OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
|
||||||
|
return orgProvider != null && orgProvider.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOrganizationRelatedGroup(Object o) {
|
||||||
|
if (o instanceof GroupRepresentation rep) {
|
||||||
|
return attributeContains(rep.getAttributes());
|
||||||
|
} else if (o instanceof GroupModel model) {
|
||||||
|
return attributeContains(model.getAttributes());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean attributeContains(Map<String, List<String>> attributes) {
|
||||||
|
return attributes != null && attributes.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkModel(GroupModel model) {
|
||||||
|
if (isOrganizationRelatedGroup(model)) {
|
||||||
|
throw ErrorResponse.error("Cannot manage organization related group via non Organization API.", Response.Status.FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkRep(GroupRepresentation rep) {
|
||||||
|
if (isOrganizationRelatedGroup(rep)) {
|
||||||
|
throw ErrorResponse.error("Cannot use group attribute reserved for organizations.", Response.Status.FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -319,7 +319,7 @@ public class GroupSearchTest extends AbstractGroupTest {
|
||||||
reconnectAdminClient();
|
reconnectAdminClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String buildSearchQuery(String firstAttrName, String firstAttrValue, String... furtherAttrKeysAndValues) {
|
public static String buildSearchQuery(String firstAttrName, String firstAttrValue, String... furtherAttrKeysAndValues) {
|
||||||
if (furtherAttrKeysAndValues.length % 2 != 0) {
|
if (furtherAttrKeysAndValues.length % 2 != 0) {
|
||||||
throw new IllegalArgumentException("Invalid length of furtherAttrKeysAndValues. Must be even, but is: " + furtherAttrKeysAndValues.length);
|
throw new IllegalArgumentException("Invalid length of furtherAttrKeysAndValues. Must be even, but is: " + furtherAttrKeysAndValues.length);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -245,4 +247,16 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
Assert.assertEquals(1, reps.size());
|
Assert.assertEquals(1, reps.size());
|
||||||
return reps.get(0);
|
return reps.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected GroupRepresentation createGroup(RealmResource realm, String name) {
|
||||||
|
GroupRepresentation group = new GroupRepresentation();
|
||||||
|
group.setName(name);
|
||||||
|
try (Response response = realm.groups().add(group)) {
|
||||||
|
String groupId = ApiUtil.getCreatedId(response);
|
||||||
|
|
||||||
|
// Set ID to the original rep
|
||||||
|
group.setId(groupId);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,11 +289,11 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
addMember(organization);
|
addMember(organization);
|
||||||
|
|
||||||
assertTrue(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
|
assertTrue(testRealm().groups().groups("", 0, 100, false).stream().anyMatch(group -> group.getAttributes().containsKey("kc.org")));
|
||||||
|
|
||||||
organization.delete().close();
|
organization.delete().close();
|
||||||
|
|
||||||
assertFalse(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
|
assertFalse(testRealm().groups().groups("", 0, 100, false).stream().anyMatch(group -> group.getAttributes().containsKey("kc.org")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -31,20 +31,27 @@ import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.keycloak.testsuite.admin.group.GroupSearchTest.buildSearchQuery;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.ForbiddenException;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.GroupResource;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
@ -299,4 +306,151 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
assertEquals(1, existing.getDomains().size());
|
assertEquals(1, existing.getDomains().size());
|
||||||
assertNotNull(existing.getDomain("acme.com"));
|
assertNotNull(existing.getDomain("acme.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
OrganizationRepresentation existing = testRealm().organizations().get(expected.getId()).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 ForbiddenException");
|
||||||
|
} catch (ForbiddenException 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 ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success, the group could not be updated
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot remove organization related group
|
||||||
|
group.remove();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success, the group could not be removed
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot manage organization related group permissions
|
||||||
|
group.setPermissions(new ManagementPermissionRepresentation(true));
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException 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.FORBIDDEN.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.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot manage organization related group role mappers
|
||||||
|
group.roles().realmLevel().add(null);
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot manage organization related group role mappers
|
||||||
|
group.roles().realmLevel().remove(null);
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException 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 ForbiddenException");
|
||||||
|
} catch (ForbiddenException 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 ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot add top level group with reserved attribute for organizations
|
||||||
|
try (Response response = testRealm().groups().add(orgGroupRep)) {
|
||||||
|
assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot add organization related group as a default group
|
||||||
|
testRealm().addDefaultGroup(orgGroupRep.getId());
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot remove organization related group as a default group
|
||||||
|
testRealm().removeDefaultGroup(orgGroupRep.getId());
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException 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 ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot leave organization related group
|
||||||
|
testRealm().users().get(userRep.getId()).leaveGroup(orgGroupRep.getId());
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException ex) {
|
||||||
|
// success
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue