From d4b7e1a7d9ec7ab262edc6f661237903356903b2 Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Wed, 24 Apr 2024 09:46:35 +0200 Subject: [PATCH] Prevent to manage groups associated with organizations from different APIs Closes #28734 Signed-off-by: Martin Kanis --- .../jpa/entities/OrganizationEntity.java | 1 + .../jpa/JpaOrganizationProvider.java | 41 +++-- .../resources/admin/GroupResource.java | 7 +- .../resources/admin/GroupsResource.java | 7 + .../resources/admin/RealmAdminResource.java | 10 +- .../resources/admin/UserResource.java | 6 + .../org/keycloak/utils/OrganizationUtils.java | 74 +++++++++ .../admin/group/GroupSearchTest.java | 2 +- .../admin/AbstractOrganizationTest.java | 14 ++ .../admin/OrganizationMemberTest.java | 4 +- .../organization/admin/OrganizationTest.java | 154 ++++++++++++++++++ 11 files changed, 296 insertions(+), 24 deletions(-) create mode 100644 services/src/main/java/org/keycloak/utils/OrganizationUtils.java diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java index ec3b11df23c..57f3731095d 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java @@ -36,6 +36,7 @@ import jakarta.persistence.Table; @Entity @NamedQueries({ @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" + " 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" + diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java index 1547c61e63c..d51a243a293 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java @@ -23,7 +23,6 @@ import static org.keycloak.utils.StreamsUtil.closing; import java.util.List; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -73,10 +72,15 @@ public class JpaOrganizationProvider implements OrganizationProvider { throw new ModelValidationException("Name can not be null"); } - GroupModel group = createOrganizationGroup(name); - OrganizationEntity entity = new OrganizationEntity(); + if (getByName(name) != null) { + throw new ModelDuplicateException("A organization with the same name already exists."); + } + OrganizationEntity entity = new OrganizationEntity(); entity.setId(KeycloakModelUtils.generateId()); + + GroupModel group = createOrganizationGroup(entity.getId()); + entity.setGroupId(group.getId()); entity.setRealmId(realm.getId()); entity.setName(name); @@ -335,21 +339,11 @@ public class JpaOrganizationProvider implements OrganizationProvider { return entity; } - private GroupModel createOrganizationGroup(String name) { - throwExceptionIfObjectIsNull(name, "Name of the group"); + private GroupModel createOrganizationGroup(String orgId) { + GroupModel group = groupProvider.createGroup(realm, null, orgId); + group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, orgId); - String groupName = getCanonicalGroupName(name); - 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; + return group; } private GroupModel getOrganizationGroup(OrganizationModel organization) { @@ -370,4 +364,17 @@ public class JpaOrganizationProvider implements OrganizationProvider { throw new ModelException(String.format("%s cannot be null", objectName)); } } + + private OrganizationEntity getByName(String name) { + TypedQuery query = em.createNamedQuery("getByOrgName", OrganizationEntity.class); + + query.setParameter("name", name); + query.setParameter("realmId", realm.getId()); + + try { + return query.getSingleResult(); + } catch (NoResultException nre) { + return null; + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java index 097e7f0f876..6ea630043b4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java @@ -61,6 +61,7 @@ import java.util.Set; import java.util.stream.Stream; import org.keycloak.utils.GroupUtils; +import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupRep; 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); } + checkForOrgRelatedGroupRep(session, rep); + if (!Objects.equals(groupName, group.getName())) { boolean exists = siblings().filter(s -> !Objects.equals(s.getId(), group.getId())) .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); } + checkForOrgRelatedGroupRep(session, rep); + try { Response.ResponseBuilder builder = Response.status(204); 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") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.groups().requireManage(group); + AdminPermissionManagement permissions = AdminPermissions.management(session, realm); permissions.groups().setPermissionsEnabled(group, ref.isEnabled()); if (ref.isEnabled()) { @@ -375,6 +381,5 @@ public class GroupResource { return new ManagementPermissionReference(); } } - } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java index 2cda94850ff..3b1965113a5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -52,6 +52,8 @@ import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluato import org.keycloak.utils.GroupUtils; 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) { throw new NotFoundException("Could not find group by id"); } + + checkForOrgRelatedGroupModel(session, group); + 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); } + checkForOrgRelatedGroupRep(session, rep); + try { if (rep.getId() != null) { child = realm.getGroupById(rep.getId()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index a01550d4a17..bc2b5c618d9 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -17,6 +17,7 @@ package org.keycloak.services.resources.admin; import static org.keycloak.util.JsonSerialization.readValue; +import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupModel; import java.io.InputStream; import java.security.cert.X509Certificate; @@ -30,7 +31,6 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import jakarta.enterprise.inject.Default; import jakarta.ws.rs.DefaultValue; import org.eclipse.microprofile.openapi.annotations.Operation; 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.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.PathSegment; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; 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.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; -import org.keycloak.models.utils.StripSecretsUtils; import org.keycloak.partialimport.ErrorResponseException; import org.keycloak.partialimport.PartialImportResult; import org.keycloak.partialimport.PartialImportResults; @@ -1053,6 +1051,9 @@ public class RealmAdminResource { if (group == null) { throw new NotFoundException("Group not found"); } + + checkForOrgRelatedGroupModel(session, group); + realm.addDefaultGroup(group); adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success(); @@ -1070,6 +1071,9 @@ public class RealmAdminResource { if (group == null) { throw new NotFoundException("Group not found"); } + + checkForOrgRelatedGroupModel(session, group); + realm.removeDefaultGroup(group); adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 50073edd1b1..0bc280f86d6 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -128,6 +128,7 @@ import java.util.stream.Stream; import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID; import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME; import static org.keycloak.userprofile.UserProfileContext.USER_API; +import static org.keycloak.utils.OrganizationUtils.checkForOrgRelatedGroupModel; /** * Base resource for managing users @@ -1017,6 +1018,8 @@ public class UserResource { } auth.groups().requireManageMembership(group); + checkForOrgRelatedGroupModel(session, group); + try { if (user.isMemberOf(group)){ user.leaveGroup(group); @@ -1044,6 +1047,9 @@ public class UserResource { throw new NotFoundException("Group not found"); } auth.groups().requireManageMembership(group); + + checkForOrgRelatedGroupModel(session, group); + if (!RoleUtils.isDirectMember(user.getGroupsStream(),group)){ user.joinGroup(group); adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(session.getContext().getUri()).success(); diff --git a/services/src/main/java/org/keycloak/utils/OrganizationUtils.java b/services/src/main/java/org/keycloak/utils/OrganizationUtils.java new file mode 100644 index 00000000000..91320ff01d3 --- /dev/null +++ b/services/src/main/java/org/keycloak/utils/OrganizationUtils.java @@ -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> 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); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java index 194be84a142..c3f1baf226e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java @@ -319,7 +319,7 @@ public class GroupSearchTest extends AbstractGroupTest { 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) { throw new IllegalArgumentException("Invalid length of furtherAttrKeysAndValues. Must be even, but is: " + furtherAttrKeysAndValues.length); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 79db9514dbf..08e72e8134c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -31,6 +31,8 @@ import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.admin.client.resource.UsersResource; 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.OrganizationRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -245,4 +247,16 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { Assert.assertEquals(1, reps.size()); 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; + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java index c18dfe1258b..b2768360d11 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java @@ -289,11 +289,11 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); 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(); - 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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java index 979ad8e196f..dd533564aa5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java @@ -31,20 +31,27 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.keycloak.testsuite.admin.group.GroupSearchTest.buildSearchQuery; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.NotFoundException; 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.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.OrganizationRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; @EnableFeature(Feature.ORGANIZATION) @@ -299,4 +306,151 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(1, existing.getDomains().size()); assertNotNull(existing.getDomain("acme.com")); } + + @Test + public void testManageOrgGroupsViaDifferentAPIs() { + // test realm contains some groups initially + List getAllBefore = testRealm().groups().groups(); + long countBefore = testRealm().groups().count().get("count"); + + List 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 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 + } + } }