keycloak-aplcache

KEYCLOAK-5017 Adding user to newly created group caused sync

12/12/2017 1:51:15 PM

Details

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 a92dfb0..f6a7c53 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
@@ -391,7 +391,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
 
         // Create or update KC groups to LDAP including their attributes
         for (GroupModel kcGroup : realm.getTopLevelGroups()) {
-            processLdapGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult);
+            processKeycloakGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult);
         }
 
         // If dropNonExisting, then drop all groups, which doesn't exist in KC from LDAP as well
@@ -409,7 +409,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
         // Finally process memberships,
         if (config.isPreserveGroupsInheritance()) {
             for (GroupModel kcGroup : realm.getTopLevelGroups()) {
-                processLdapGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap);
+                processKeycloakGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap);
             }
         }
 
@@ -419,7 +419,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
     // For given kcGroup check if it exists in LDAP (map) by name
     // If not, create it in LDAP including attributes. Otherwise update attributes in LDAP.
     // Process this recursively for all subgroups of KC group
-    private void processLdapGroupSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap, Set<String> ldapGroupNames, SynchronizationResult syncResult) {
+    private void processKeycloakGroupSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap, Set<String> ldapGroupNames, SynchronizationResult syncResult) {
         String groupName = kcGroup.getName();
 
         // extract group attributes to be updated to LDAP
@@ -449,12 +449,12 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
 
         // process KC subgroups
         for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
-            processLdapGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult);
+            processKeycloakGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult);
         }
     }
 
-    // Sync memberships update. Update memberships of group in LDAP based on subgroups from KC. Do it recursively
-    private void processLdapGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap) {
+    // Update memberships of group in LDAP based on subgroups from KC. Do it recursively
+    private void processKeycloakGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap) {
         LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
         Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
 
@@ -481,7 +481,25 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
         }
 
         for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
-            processLdapGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
+            processKeycloakGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
+        }
+    }
+
+    // Recursively check if parent group exists in LDAP. If yes, then return current group. If not, then recursively call this method
+    // for the predecessor. Result is the highest group, which doesn't yet exists in LDAP (and hence requires sync to LDAP)
+    private GroupModel getHighestPredecessorNotExistentInLdap(GroupModel group) {
+        GroupModel parentGroup = group.getParent();
+        if (parentGroup == null) {
+            return group;
+        }
+
+        LDAPObject ldapGroup = loadLDAPGroupByName(parentGroup.getName());
+        if (ldapGroup != null) {
+            // Parent exists in LDAP. Let's return current group
+            return group;
+        } else {
+            // Parent doesn't exists in LDAP. Let's recursively go up.
+            return getHighestPredecessorNotExistentInLdap(parentGroup);
         }
     }
 
@@ -500,11 +518,34 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
         return membershipType.getGroupMembers(realm, this, ldapGroup, firstResult, maxResults);
     }
 
-    public void addGroupMappingInLDAP(RealmModel realm, String groupName, LDAPObject ldapUser) {
+    public void addGroupMappingInLDAP(RealmModel realm, GroupModel kcGroup, LDAPObject ldapUser) {
+        String groupName = kcGroup.getName();
         LDAPObject ldapGroup = loadLDAPGroupByName(groupName);
+
         if (ldapGroup == null) {
-            syncDataFromKeycloakToFederationProvider(realm);
-            ldapGroup = loadLDAPGroupByName(groupName);
+            // Needs to partially sync Keycloak groups to LDAP
+            if (config.isPreserveGroupsInheritance()) {
+                GroupModel highestGroupToSync = getHighestPredecessorNotExistentInLdap(kcGroup);
+
+                logger.debugf("Will sync group '%s' and it's subgroups from DB to LDAP", highestGroupToSync.getName());
+
+                Map<String, LDAPObject> syncedLDAPGroups = new HashMap<>();
+                processKeycloakGroupSyncToLDAP(highestGroupToSync, syncedLDAPGroups, new HashSet<>(), new SynchronizationResult());
+                processKeycloakGroupMembershipsSyncToLDAP(highestGroupToSync, syncedLDAPGroups);
+
+                ldapGroup = loadLDAPGroupByName(groupName);
+
+                // Finally update LDAP membership in the parent group
+                if (highestGroupToSync.getParent() != null) {
+                    LDAPObject ldapParentGroup = loadLDAPGroupByName(highestGroupToSync.getParent().getName());
+                    LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), getMembershipUserLdapAttribute(), ldapParentGroup, ldapGroup, true);
+                }
+            } else {
+                // No care about group inheritance. Let's just sync current group
+                logger.debugf("Will sync group '%s' from DB to LDAP", groupName);
+                processKeycloakGroupSyncToLDAP(kcGroup, new HashMap<>(), new HashSet<>(), new SynchronizationResult());
+                ldapGroup = loadLDAPGroupByName(groupName);
+            }
         }
 
         String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
@@ -614,7 +655,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
             if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
                 // We need to create new role mappings in LDAP
                 cachedLDAPGroupMappings = null;
-                addGroupMappingInLDAP(realm, group.getName(), ldapUser);
+                addGroupMappingInLDAP(realm, group, ldapUser);
             } else {
                 super.joinGroup(group);
             }
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
index 746b302..51f8fdd 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
@@ -317,7 +317,11 @@ public class LDAPGroupMapperSyncTest {
             LDAPTestUtils.removeAllLDAPUsers(ldapProvider, realm);
             LDAPObject johnLdap = LDAPTestUtils.addLDAPUser(ldapProvider, realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
             LDAPTestUtils.updateLDAPPassword(ldapProvider, johnLdap, "Password1");
-            groupMapper.addGroupMappingInLDAP(realm, "group11", johnLdap);
+
+            GroupMapperConfig groupMapperConfig = new GroupMapperConfig(mapperModel);
+            LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName("group11");
+            LDAPUtils.addMember(ldapProvider, groupMapperConfig.getMembershipTypeLdapAttribute(), groupMapperConfig.getMembershipLdapAttribute(),
+                    groupMapperConfig.getMembershipUserLdapAttribute(ldapProvider.getLdapIdentityStore().getConfig()), ldapGroup, johnLdap, true);
 
             // Assert groups not yet imported to Keycloak DB
             Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
index 7380ba7..b7c9742 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
@@ -243,8 +243,8 @@ public class LDAPGroupMapperTest {
             GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
 
             LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
-            groupMapper.addGroupMappingInLDAP(appRealm, "group1", maryLdap);
-            groupMapper.addGroupMappingInLDAP(appRealm, "group11", maryLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, group1, maryLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, group11, maryLdap);
 
             // Add some group mapping to model
             mary.joinGroup(group12);
@@ -304,18 +304,18 @@ public class LDAPGroupMapperTest {
             LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
             GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
 
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
+            GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
+            GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
+
             LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
-            groupMapper.addGroupMappingInLDAP(appRealm, "group11", robLdap);
-            groupMapper.addGroupMappingInLDAP(appRealm, "group12", robLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, group11, robLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, group12, robLdap);
 
             // Get user and check that he has requested groupa from LDAP
             UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
             Set<GroupModel> robGroups = rob.getGroups();
 
-            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
-            GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
-            GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
-
             Assert.assertFalse(robGroups.contains(group1));
             Assert.assertTrue(robGroups.contains(group11));
             Assert.assertTrue(robGroups.contains(group12));
@@ -450,6 +450,79 @@ public class LDAPGroupMapperTest {
     }
 
 
+    // KEYCLOAK-5017
+    @Test
+    public void test06_addingUserToNewKeycloakGroup() throws Exception {
+        // Add some groups to Keycloak
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            GroupModel group3 = appRealm.createGroup("group3");
+            session.realms().addTopLevelGroup(appRealm, group3);
+            GroupModel group31 = appRealm.createGroup("group31");
+            group3.addChild(group31);
+            GroupModel group32 = appRealm.createGroup("group32");
+            group3.addChild(group32);
+
+            GroupModel group4 = appRealm.createGroup("group4");
+            session.realms().addTopLevelGroup(appRealm, group4);
+
+            GroupModel group14 = appRealm.createGroup("group14");
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
+            group1.addChild(group14);
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // Add user to some newly created KC groups
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+
+            GroupModel group4 =  KeycloakModelUtils.findGroupByPath(appRealm, "/group4");
+            john.joinGroup(group4);
+
+            GroupModel group31 = KeycloakModelUtils.findGroupByPath(appRealm, "/group3/group31");
+            GroupModel group32 = KeycloakModelUtils.findGroupByPath(appRealm, "/group3/group32");
+
+            john.joinGroup(group31);
+            john.joinGroup(group32);
+
+            GroupModel group14 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group14");
+            john.joinGroup(group14);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // Check user group memberships
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+
+            GroupModel group14 =  KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group14");
+            GroupModel group3 =  KeycloakModelUtils.findGroupByPath(appRealm, "/group3");
+            GroupModel group31 = KeycloakModelUtils.findGroupByPath(appRealm, "/group3/group31");
+            GroupModel group32 = KeycloakModelUtils.findGroupByPath(appRealm, "/group3/group32");
+            GroupModel group4 =  KeycloakModelUtils.findGroupByPath(appRealm, "/group4");
+
+            Set<GroupModel> groups = john.getGroups();
+            Assert.assertTrue(groups.contains(group14));
+            Assert.assertFalse(groups.contains(group3));
+            Assert.assertTrue(groups.contains(group31));
+            Assert.assertTrue(groups.contains(group32));
+            Assert.assertTrue(groups.contains(group4));
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
     private void deleteGroupMappingsInLDAP(GroupLDAPStorageMapper groupMapper, LDAPObject ldapUser, String groupName) {
         LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName(groupName);
         groupMapper.deleteGroupMappingInLDAP(ldapUser, ldapGroup);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/noimport/LDAPGroupMapperNoImportTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/noimport/LDAPGroupMapperNoImportTest.java
index 6cf6435..38ae0da 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/noimport/LDAPGroupMapperNoImportTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/ldap/noimport/LDAPGroupMapperNoImportTest.java
@@ -167,8 +167,8 @@ public class LDAPGroupMapperNoImportTest {
             GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
 
             LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
-            groupMapper.addGroupMappingInLDAP(appRealm, "group1", maryLdap);
-            groupMapper.addGroupMappingInLDAP(appRealm, "group11", maryLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, KeycloakModelUtils.findGroupByPath(appRealm, "/group1"), maryLdap);
+            groupMapper.addGroupMappingInLDAP(appRealm, KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"), maryLdap);
         } finally {
             keycloakRule.stopSession(session, true);
         }
diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LdapManyGroupsInitializerCommand.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LdapManyGroupsInitializerCommand.java
new file mode 100644
index 0000000..26c778c
--- /dev/null
+++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/LdapManyGroupsInitializerCommand.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 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.util.cli;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper;
+
+/**
+ * The command requires that:
+ * - Realm has 1 LDAP storage provider defined
+ * - The LDAP provider has group-mapper named "groupsMapper", with:
+ * -- "LDAP Groups DN" pointing to same DN, like this command <groups-dn> .
+ * -- It's supposed to PreserveGroupsInheritance on
+ *
+ * It will create top-groups-count "root" groups and "subgroups-in-every-top-group" groups in every child.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LdapManyGroupsInitializerCommand extends AbstractCommand  {
+
+    @Override
+    public String getName() {
+        return "createLdapGroups";
+    }
+
+    @Override
+    public String printUsage() {
+        return super.printUsage() + " <realm-name> <groups-dn> <start-offset-top-groups> <top-groups-count> <subgroups-in-every-top-group>.\nSee javadoc of class LdapManyGroupsInitializerCommand for additional details.";
+    }
+
+    @Override
+    protected void doRunCommand(KeycloakSession session) {
+        String realmName = getArg(0);
+        String groupsDn = getArg(1);
+        int startOffsetTopGroups = getIntArg(2);
+        int topGroupsCount = getIntArg(3);
+        int subgroupsInEveryGroup = getIntArg(4);
+
+        RealmModel realm = session.realms().getRealmByName(realmName);
+        List<ComponentModel> components = realm.getComponents(realm.getId(), UserStorageProvider.class.getName());
+        if (components.size() != 1) {
+            log.errorf("Expected 1 LDAP Provider, but found: %d providers", components.size());
+            throw new HandledException();
+        }
+        ComponentModel ldapModel = components.get(0);
+
+        // Check that street mapper exists. It's required for now, so that "street" attribute is written to the LDAP
+        ComponentModel groupMapperModel = getMapperModel(realm, ldapModel, "groupsMapper");
+
+
+        // Create groups
+        for (int i=startOffsetTopGroups ; i<startOffsetTopGroups+topGroupsCount ; i++) {
+            final int iFinal = i;
+            KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
+
+                LDAPStorageProvider ldapProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, ldapModel);
+                RealmModel appRealm = session.realms().getRealmByName(realmName);
+                GroupLDAPStorageMapper groupMapper = (GroupLDAPStorageMapper) session.getProvider(LDAPStorageMapper.class, groupMapperModel);
+
+                Set<String> childGroupDns = new HashSet<>();
+
+                for (int j=0 ; j<subgroupsInEveryGroup ; j++) {
+                    String groupName = "group-" + iFinal + "-" + j;
+                    LDAPObject createdGroup = groupMapper.createLDAPGroup(groupName, new HashMap<>());
+                    childGroupDns.add(createdGroup.getDn().toString());
+                }
+
+                String topGroupName = "group-" + iFinal;
+
+                Map<String, Set<String>> groupAttrs = new HashMap<>();
+                groupAttrs.put("member", new HashSet<>(childGroupDns));
+
+                groupMapper.createLDAPGroup(topGroupName, groupAttrs);
+
+            });
+        }
+    }
+
+
+    private ComponentModel getMapperModel(RealmModel realm, ComponentModel ldapModel, String mapperName) {
+        List<ComponentModel> ldapMappers = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName());
+        Optional<ComponentModel> optional = ldapMappers.stream().filter((ComponentModel mapper) -> {
+            return mapper.getName().equals(mapperName);
+        }).findFirst();
+
+        if (!optional.isPresent()) {
+            log.errorf("Not present LDAP mapper called '%s'", mapperName);
+            throw new HandledException();
+        }
+
+        return optional.get();
+    }
+}
diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
index 9da8a84..751e397 100644
--- a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
+++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
@@ -66,7 +66,8 @@ public class TestsuiteCLI {
             CacheCommands.GetCacheCommand.class,
             CacheCommands.CacheRealmObjectsCommand.class,
             ClusterProviderTaskCommand.class,
-            LdapManyObjectsInitializerCommand.class
+            LdapManyObjectsInitializerCommand.class,
+            LdapManyGroupsInitializerCommand.class
     };
 
     private final KeycloakSessionFactory sessionFactory;