diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java index b09b0b68e4f..03f7afdc583 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java @@ -807,6 +807,9 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements } protected boolean isGroupInGroupPath(RealmModel realm, GroupModel group) { + if (group.getType() == GroupModel.Type.ORGANIZATION) { + return false; // always skip organization groups as those are internal groups. + } if (config.isTopLevelGroupsPath()) { return true; // any group is in the path of the top level path. } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java index 657aa69d274..b076658291f 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java @@ -20,6 +20,7 @@ package org.keycloak.admin.client.resource; import java.util.List; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -39,6 +40,10 @@ public interface OrganizationMembersResource { @Consumes(MediaType.APPLICATION_JSON) Response addMember(String userId); + @Path("{member-id}") + @DELETE + Response removeMember(@PathParam("member-id") String memberId); + /** * Return all members in the organization. * diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java index e7333cac4ac..e4460ab8423 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java @@ -458,6 +458,17 @@ public class LDAPTestUtils { } } + public static LDAPObject getLdapGroupByName(KeycloakSession session, RealmModel realm, String mapperName, String groupName) { + ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm); + ComponentModel mapperModel = getSubcomponentByName(realm, ldapModel, mapperName); + LDAPStorageProvider ldapProvider = getLdapProvider(session, ldapModel); + if (GroupLDAPStorageMapperFactory.PROVIDER_ID.equals(mapperModel.getProviderId())) { + return getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName(groupName); + } else { + return getRoleMapper(mapperModel, ldapProvider, realm).loadLDAPRoleByName(groupName); + } + } + public static LDAPObject updateLDAPGroup(KeycloakSession session, RealmModel appRealm, ComponentModel ldapModel, LDAPObject ldapObject) { ComponentModel mapperModel = getSubcomponentByName(appRealm, ldapModel, "groupsMapper"); LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java new file mode 100644 index 00000000000..5f65fc48de9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025 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.testsuite.organization.member; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.ws.rs.core.Response; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.admin.client.resource.OrganizationResource; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.MemberRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.testsuite.federation.ldap.LDAPTestContext; +import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest; +import org.keycloak.testsuite.util.LDAPRule; +import org.keycloak.testsuite.util.LDAPTestUtils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; + +public class OrganizationMemberWithLdapTest extends AbstractOrganizationTest { + + @ClassRule + public static LDAPRule ldapRule = new LDAPRule(); + + @Override + public void importTestRealms() { + super.importTestRealms(); + + // add an LDAP provider with a group mapper + Map cfg = ldapRule.getConfig(); + testingClient.testing().ldap(TEST_REALM_NAME).createLDAPProvider(cfg, true); + testingClient.testing().ldap(TEST_REALM_NAME).prepareGroupsLDAPTest(); + } + + @Test + public void testLdapUserJoiningAndLeavingOrganization() { + testingClient.server().run(session -> { + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + + // ensure groups mapper is in LDAP_ONLY mode - we want to check that upon joining the org, the org group is NOT pushed to LDAP. + ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ctx.getLdapModel(), "groupsMapper"); + LDAPTestUtils.updateConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString()); + appRealm.updateComponent(mapperModel); + + // check that the LDAP provider is working - i.e. users are available and groups have been properly synced. + UserModel john = session.users().getUserByUsername(appRealm, "johnkeycloak"); + assertThat(john, notNullValue()); + GroupModel testGroup = KeycloakModelUtils.findGroupByPath(session, appRealm, "/group1"); + assertThat(testGroup, notNullValue()); + }); + + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + OrganizationRepresentation orgRepresentation = organization.toRepresentation(); + UserRepresentation ldapUser = testRealm().users().searchByUsername("johnkeycloak", true).get(0); + + // make the LDAP user join the organization and check it was successful. + try (Response response = organization.members().addMember(ldapUser.getId())) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + List orgMemberships = organization.members().member(ldapUser.getId()).getOrganizations(); + assertThat(orgMemberships, notNullValue()); + assertThat(orgMemberships, hasSize(1)); + assertThat(orgMemberships.get(0).getId(), equalTo(orgRepresentation.getId())); + + // check that the org group was NOT pushed to LDAP as a result of joining the org. + AtomicReference orgId = new AtomicReference<>(orgRepresentation.getId()); + testingClient.server(TEST_REALM_NAME).run(session -> { + LDAPTestContext context = LDAPTestContext.init(session); + assertThat(LDAPTestUtils.getLdapGroupByName(session, context.getRealm(), "groupsMapper", orgId.get()), is(nullValue())); + }); + + // make the user leave the organization and check it was successful. + try (Response response = organization.members().removeMember(ldapUser.getId())) { + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + List orgMembers = organization.members().list(-1, -1); + assertThat(orgMembers, hasSize(0)); + } + +}