From 4302b440eefe352bbd2ef7cdec323b8f1721b31f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sat, 5 Nov 2016 20:04:53 -0400 Subject: [PATCH] ldap port --- .../UserAttributeLDAPStorageMapper.java | 1 + .../models/cache/infinispan/RealmAdapter.java | 24 +- .../cache/infinispan/RealmCacheSession.java | 2 +- .../models/cache/infinispan/UserAdapter.java | 5 + .../cache/infinispan/UserCacheSession.java | 5 +- .../org/keycloak/models/jpa/RealmAdapter.java | 5 +- .../mongo/keycloak/adapters/RealmAdapter.java | 5 +- .../models/cache/CachedUserModel.java | 2 + .../org/keycloak/models/cache/UserCache.java | 7 + .../models/utils/KeycloakModelUtils.java | 24 ++ .../credential/OTPCredentialProvider.java | 8 +- .../PasswordCredentialProvider.java | 2 +- .../keycloak/storage/UserStorageManager.java | 6 + .../ldap/LDAPGroupMapper2WaySyncTest.java | 259 ++++++++++++ .../storage/ldap/LDAPGroupMapperSyncTest.java | 320 +++++++++++++++ .../storage/ldap/LDAPGroupMapperTest.java | 368 +++++++++++++++++ .../ldap/LDAPMultipleAttributesTest.java | 249 ++++++++++++ .../ldap/LDAPProvidersIntegrationTest.java | 51 +-- .../storage/ldap/LDAPRoleMappingsTest.java | 362 +++++++++++++++++ .../federation/storage/ldap/LDAPSyncTest.java | 378 ++++++++++++++++++ .../storage/ldap/LDAPTestUtils.java | 15 +- 21 files changed, 2063 insertions(+), 35 deletions(-) create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java index 6c5fbb2f73..cd41c5af43 100644 --- a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java +++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java @@ -157,6 +157,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper { // throw ModelDuplicateException if there is different user in model with same email protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) { + if (email == null) return; if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) { // lowercase before search email = KeycloakModelUtils.toLowerCaseSafe(email); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 2b2179ce01..8252f6b572 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -28,6 +28,7 @@ import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; @@ -39,6 +40,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.UserStorageProvider; import java.security.Key; import java.security.PrivateKey; @@ -62,10 +64,12 @@ public class RealmAdapter implements CachedRealmModel { protected RealmCacheSession cacheSession; protected RealmModel updated; protected RealmCache cache; + protected KeycloakSession session; - public RealmAdapter(CachedRealm cached, RealmCacheSession cacheSession) { + public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) { this.cached = cached; this.cacheSession = cacheSession; + this.session = session; } @Override @@ -1333,12 +1337,28 @@ public class RealmAdapter implements CachedRealmModel { @Override public ComponentModel addComponentModel(ComponentModel model) { getDelegateForUpdate(); + evictUsers(model); return updated.addComponentModel(model); } + public void evictUsers(ComponentModel model) { + String parentId = model.getParentId(); + evictUsers(parentId); + } + + public void evictUsers(String parentId) { + if (parentId != null && !parentId.equals(getId())) { + ComponentModel parent = getComponent(parentId); + if (parent != null && UserStorageProvider.class.getName().equals(parent.getProviderType())) { + session.getUserCache().evict(this); + } + } + } + @Override public void updateComponent(ComponentModel component) { getDelegateForUpdate(); + evictUsers(component); updated.updateComponent(component); } @@ -1346,6 +1366,7 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponent(ComponentModel component) { getDelegateForUpdate(); + evictUsers(component); updated.removeComponent(component); } @@ -1353,6 +1374,7 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponents(String parentId) { getDelegateForUpdate(); + evictUsers(parentId); updated.removeComponents(parentId); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 565fd483d9..9321f47197 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -389,7 +389,7 @@ public class RealmCacheSession implements CacheRealmProvider { } else if (managedRealms.containsKey(id)) { return managedRealms.get(id); } - RealmAdapter adapter = new RealmAdapter(cached, this); + RealmAdapter adapter = new RealmAdapter(session, cached, this); if (wasCached) { CachedRealmModel.RealmCachedEvent event = new CachedRealmModel.RealmCachedEvent() { @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 6030a66e8e..bc494404d4 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -63,6 +63,11 @@ public class UserAdapter implements CachedUserModel { return updated; } + @Override + public boolean isMarkedForEviction() { + return updated != null; + } + @Override public void invalidate() { getDelegateForUpdate(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 0bb71c0b1b..b51d911c20 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -115,7 +115,10 @@ public class UserCacheSession implements UserCache { } } - + @Override + public void evict(RealmModel realm) { + realmInvalidations.add(realm.getId()); + } protected void runInvalidations() { for (String realmId : realmInvalidations) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index b8670dc115..6de22a9200 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -2046,7 +2046,10 @@ public class RealmAdapter implements RealmModel, JpaModel { } c.setName(model.getName()); c.setParentId(model.getParentId()); - if (model.getParentId() == null) c.setParentId(this.getId()); + if (model.getParentId() == null) { + c.setParentId(this.getId()); + model.setParentId(this.getId()); + } c.setProviderType(model.getProviderType()); c.setProviderId(model.getProviderId()); c.setSubType(model.getSubType()); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 2fa756192d..205f6b3091 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -1964,7 +1964,10 @@ public class RealmAdapter extends AbstractMongoAdapter impleme } updateComponentEntity(entity, model); model.setId(entity.getId()); - if (model.getParentId() == null) entity.setParentId(this.getId()); + if (model.getParentId() == null) { + entity.setParentId(this.getId()); + model.setParentId(this.getId()); + } realm.getComponentEntities().add(entity); updateRealm(); ComponentUtil.notifyCreated(session, this, model); diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java index 67431a90f9..8434d9de4c 100644 --- a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java @@ -35,6 +35,8 @@ public interface CachedUserModel extends UserModel { */ UserModel getDelegateForUpdate(); + boolean isMarkedForEviction(); + /** * Invalidate the cache for this model * diff --git a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java index f309079d94..260b0bef17 100755 --- a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java @@ -32,5 +32,12 @@ public interface UserCache extends UserProvider { * @param user */ void evict(RealmModel realm, UserModel user); + + /** + * Evict users of a specific realm + * + * @param realm + */ + void evict(RealmModel realm); void clear(); } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index a962300249..b57d2b1e62 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -52,6 +52,7 @@ import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.CertificateRepresentation; +import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.transaction.JtaTransactionManagerLookup; import javax.crypto.spec.SecretKeySpec; @@ -372,6 +373,29 @@ public final class KeycloakModelUtils { } return null; } + + public static UserStorageProviderModel findUserStorageProviderByName(String displayName, RealmModel realm) { + if (displayName == null) { + return null; + } + + for (UserStorageProviderModel fedProvider : realm.getUserStorageProviders()) { + if (displayName.equals(fedProvider.getName())) { + return fedProvider; + } + } + return null; + } + + public static UserStorageProviderModel findUserStorageProviderById(String fedProviderId, RealmModel realm) { + for (UserStorageProviderModel fedProvider : realm.getUserStorageProviders()) { + if (fedProviderId.equals(fedProvider.getId())) { + return fedProvider; + } + } + return null; + } + public static ComponentModel createComponentModel(String name, String parentId, String providerId, String providerType, String... config) { ComponentModel mapperModel = new ComponentModel(); mapperModel.setParentId(parentId); diff --git a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java index f71f84a897..fcd1e186cb 100644 --- a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java @@ -43,8 +43,9 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu protected KeycloakSession session; protected List getCachedCredentials(UserModel user, String type) { - if (!(user instanceof CachedUserModel)) return Collections.EMPTY_LIST; + if (!(user instanceof CachedUserModel)) return null; CachedUserModel cached = (CachedUserModel)user; + if (cached.isMarkedForEviction()) return null; List rtn = (List)cached.getCachedWith().get(OTPCredentialProvider.class.getName() + "." + type); if (rtn == null) return Collections.EMPTY_LIST; return rtn; @@ -186,8 +187,9 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu } protected boolean configuredForTOTP(RealmModel realm, UserModel user) { - return !getCachedCredentials(user, CredentialModel.TOTP).isEmpty() - || !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP).isEmpty(); + List cachedCredentials = getCachedCredentials(user, CredentialModel.TOTP); + if (cachedCredentials == null) return !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP).isEmpty(); + return !cachedCredentials.isEmpty(); } public static boolean validOTP(RealmModel realm, String token, String secret) { diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java index d0558abd8b..84cd0f06a5 100644 --- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java @@ -59,7 +59,7 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia public CredentialModel getPassword(RealmModel realm, UserModel user) { List passwords = null; - if (user instanceof CachedUserModel) { + if (user instanceof CachedUserModel && !((CachedUserModel)user).isMarkedForEviction()) { CachedUserModel cached = (CachedUserModel)user; passwords = (List)cached.getCachedWith().get(PASSWORD_CACHE_KEY); diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 8394bbb4a2..e3eeaecb71 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -138,6 +138,12 @@ public class UserStorageManager implements UserProvider, OnUserCache { if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, user); StorageId storageId = new StorageId(user.getId()); if (storageId.getProviderId() == null) { + if (user.getFederationLink() != null) { + UserStorageProvider provider = getStorageProvider(session, realm, user.getFederationLink()); + if (provider != null && provider instanceof UserRegistrationProvider) { + ((UserRegistrationProvider)provider).removeUser(realm, user); + } + } return localStorage().removeUser(realm, user); } UserRegistrationProvider registry = (UserRegistrationProvider)getStorageProvider(session, realm, storageId.getProviderId()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java new file mode 100755 index 0000000000..7534a93e44 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.user.SynchronizationResult; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +import java.util.Map; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPGroupMapper2WaySyncTest { + + @ClassRule + public static LDAPRule ldapRule = new LDAPRule(); + + private static ComponentModel ldapModel = null; + private static String descriptionAttrName = null; + + @Rule + public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + ldapConfig.putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "4"); // Issues with pagination on ApacheDS + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + + ldapModel = appRealm.addComponentModel(model); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Remove all LDAP groups + LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + + // Add some groups for testing into Keycloak + removeAllModelGroups(appRealm); + + GroupModel group1 = appRealm.createGroup("group1"); + appRealm.moveGroup(group1, null); + group1.setSingleAttribute(descriptionAttrName, "group1 - description1"); + + GroupModel group11 = appRealm.createGroup("group11"); + appRealm.moveGroup(group11, group1); + + GroupModel group12 = appRealm.createGroup("group12"); + appRealm.moveGroup(group12, group1); + group12.setSingleAttribute(descriptionAttrName, "group12 - description12"); + + GroupModel group2 = appRealm.createGroup("group2"); + appRealm.moveGroup(group2, null); + } + }); + + + @Test + public void test01_syncNoPreserveGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Update group mapper to skip preserve inheritance and check it will pass now + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false"); + realm.updateComponent(mapperModel); + + // Sync from Keycloak into LDAP + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + + // Delete all KC groups now + removeAllModelGroups(realm); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2")); + } finally { + keycloakRule.stopSession(session, true); + } + + + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Sync from LDAP back into Keycloak + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12"); + GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2"); + + Assert.assertEquals(0, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName)); + + // test drop non-existing works + testDropNonExisting(session, realm, mapperModel, ldapProvider); + } finally { + keycloakRule.stopSession(session, true); + } + } + + @Test + public void test02_syncWithGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Update group mapper to skip preserve inheritance and check it will pass now + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true"); + realm.updateComponent(mapperModel); + + // Sync from Keycloak into LDAP + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + + // Delete all KC groups now + removeAllModelGroups(realm); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2")); + } finally { + keycloakRule.stopSession(session, true); + } + + + + session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Sync from LDAP back into Keycloak + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2"); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName)); + + // test drop non-existing works + testDropNonExisting(session, realm, mapperModel, ldapProvider); + } finally { + keycloakRule.stopSession(session, true); + } + } + + + private static void removeAllModelGroups(RealmModel appRealm) { + for (GroupModel group : appRealm.getTopLevelGroups()) { + appRealm.removeGroup(group); + } + } + + private void testDropNonExisting(KeycloakSession session, RealmModel realm, ComponentModel mapperModel, LDAPStorageProvider ldapProvider) { + // Put some group directly to LDAP + LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "group3"); + + // Sync and assert our group is still in LDAP + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 0, 4, 0, 0); + Assert.assertNotNull(LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3")); + + // Change config to drop non-existing groups + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true"); + realm.updateComponent(mapperModel); + + // Sync and assert group removed from LDAP + syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 0, 4, 1, 0); + Assert.assertNull(LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3")); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java new file mode 100755 index 0000000000..66321245d3 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java @@ -0,0 +1,320 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.LDAPUtils; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.storage.ldap.mappers.membership.MembershipType; +import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper; +import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.user.SynchronizationResult; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPGroupMapperSyncTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static ComponentModel ldapModel = null; + private static String descriptionAttrName = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + + ldapModel = appRealm.addComponentModel(model); + + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Remove all LDAP groups + LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + + // Add some groups for testing + LDAPObject group1 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); + LDAPObject group11 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); + LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description"); + + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false); + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true); + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Before + public void before() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + List kcGroups = realm.getTopLevelGroups(); + for (GroupModel kcGroup : kcGroups) { + realm.removeGroup(kcGroup); + } + } finally { + keycloakRule.stopSession(session, true); + } + } + + @Test + public void test01_syncNoPreserveGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm); + + // Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail + LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1"); + LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12"); + LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true); + + try { + new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + Assert.fail("Not expected group sync to pass"); + } catch (ModelException expected) { + Assert.assertTrue(expected.getMessage().contains("Recursion detected")); + } + + // Update group mapper to skip preserve inheritance and check it will pass now + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false"); + realm.updateComponent(mapperModel); + + new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + + // Assert groups are imported to keycloak. All are at top level + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12"); + + Assert.assertEquals(0, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName)); + + // Cleanup - remove recursive mapping in LDAP + LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true); + + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_syncWithGroupInheritance() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm); + + // Sync groups with inheritance + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0); + + // Assert groups are imported to keycloak including their inheritance from LDAP + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12")); + GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"); + GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName)); + Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName)); + + // Update description attributes in LDAP + LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1"); + group1.setSingleAttribute(descriptionAttrName, "group1 - changed description"); + ldapProvider.getLdapIdentityStore().update(group1); + + LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12"); + group12.setAttribute(descriptionAttrName, null); + ldapProvider.getLdapIdentityStore().update(group12); + + // Sync and assert groups updated + syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0); + + // Assert attributes changed in keycloak + kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"); + Assert.assertEquals("group1 - changed description", kcGroup1.getFirstAttribute(descriptionAttrName)); + Assert.assertNull(kcGroup12.getFirstAttribute(descriptionAttrName)); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_syncWithDropNonExistingGroups() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Sync groups with inheritance + SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0); + + // Assert groups are imported to keycloak including their inheritance from LDAP + GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + + Assert.assertEquals(2, kcGroup1.getSubGroups().size()); + + // Create some new groups in keycloak + GroupModel model1 = realm.createGroup("model1"); + realm.moveGroup(model1, null); + GroupModel model2 = realm.createGroup("model2"); + kcGroup1.addChild(model2); + + // Sync groups again from LDAP. Nothing deleted + syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + LDAPTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0); + + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/model1")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2")); + + // Update group mapper to drop non-existing groups during sync + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true"); + realm.updateComponent(mapperModel); + + // Sync groups again from LDAP. Assert LDAP non-existing groups deleted + syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); + Assert.assertEquals(3, syncResult.getUpdated()); + Assert.assertTrue(syncResult.getRemoved() == 2); + + // Sync and assert groups updated + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11")); + Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/model1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2")); + } finally { + keycloakRule.stopSession(session, false); + } + } + + + + @Test + public void test04_syncNoPreserveGroupInheritanceWithLazySync() throws Exception { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel realm = session.realms().getRealmByName("test"); + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm); + + // Update group mapper to skip preserve inheritance + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false"); + realm.updateComponent(mapperModel); + + // Add user to LDAP and put him as member of group11 + 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("group11", johnLdap); + + // Assert groups not yet imported to Keycloak DB + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11")); + Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12")); + + // Load user from LDAP to Keycloak DB + UserModel john = session.users().getUserByUsername("johnkeycloak", realm); + Set johnGroups = john.getGroups(); + + // Assert just those groups, which john was memberOf exists because they were lazily created + GroupModel group1 = KeycloakModelUtils.findGroupByPath(realm, "/group1"); + GroupModel group11 = KeycloakModelUtils.findGroupByPath(realm, "/group11"); + GroupModel group12 = KeycloakModelUtils.findGroupByPath(realm, "/group12"); + Assert.assertNull(group1); + Assert.assertNotNull(group11); + Assert.assertNull(group12); + + Assert.assertEquals(1, johnGroups.size()); + Assert.assertTrue(johnGroups.contains(group11)); + + // Delete group mapping + john.leaveGroup(group11); + + } finally { + keycloakRule.stopSession(session, false); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java new file mode 100755 index 0000000000..c15a304e8a --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java @@ -0,0 +1,368 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPConfig; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.LDAPUtils; +import org.keycloak.storage.ldap.idm.model.LDAPDn; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.storage.ldap.mappers.membership.MembershipType; +import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper; +import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPGroupMapperTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static ComponentModel ldapModel = null; + private static String descriptionAttrName = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); + LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "john", "john@test.com", "password-app"); + + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + + ldapModel = appRealm.addComponentModel(model); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description"; + + // Add group mapper + LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName); + + // Remove all LDAP groups + LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper"); + + // Add some groups for testing + LDAPObject group1 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description"); + LDAPObject group11 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11"); + LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description"); + + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false); + LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true); + + // Sync LDAP groups to Keycloak DB + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper"); + new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapFedProvider, session, appRealm); + + // Delete all LDAP users + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + // Add some LDAP users for testing + LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); + + LDAPObject mary = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1"); + + LDAPObject rob = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1"); + + LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jameskeycloak", "James", "Brown", "james@email.org", null, "8910"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1"); + + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Test + public void test01_ldapOnlyGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper"); + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString()); + appRealm.updateComponent(mapperModel); + + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + // 1 - Grant some groups in LDAP + + // This group should already exists as it was imported from LDAP + GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); + john.joinGroup(group1); + + // This group should already exists as it was imported from LDAP + GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); + mary.joinGroup(group11); + + // This group should already exists as it was imported from LDAP + GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); + john.joinGroup(group12); + mary.joinGroup(group12); + + // 2 - Check that group mappings are not in local Keycloak DB (They are in LDAP). + + UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm); + Set johnDbGroups = johnDb.getGroups(); + Assert.assertEquals(0, johnDbGroups.size()); + + // 3 - Check that group mappings are in LDAP and hence available through federation + + Set johnGroups = john.getGroups(); + Assert.assertEquals(2, johnGroups.size()); + Assert.assertTrue(johnGroups.contains(group1)); + Assert.assertFalse(johnGroups.contains(group11)); + Assert.assertTrue(johnGroups.contains(group12)); + + // 4 - Check through userProvider + List group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10); + List group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10); + List group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10); + + Assert.assertEquals(1, group1Members.size()); + Assert.assertEquals("johnkeycloak", group1Members.get(0).getUsername()); + Assert.assertEquals(1, group11Members.size()); + Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername()); + Assert.assertEquals(2, group12Members.size()); + + // 4 - Delete some group mappings and check they are deleted + + john.leaveGroup(group1); + john.leaveGroup(group12); + + mary.leaveGroup(group1); + mary.leaveGroup(group12); + + johnGroups = john.getGroups(); + Assert.assertEquals(0, johnGroups.size()); + + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_readOnlyGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + System.out.println("starting test02_readOnlyGroupMappings"); + RealmModel appRealm = session.realms().getRealmByName("test"); + + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper"); + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString()); + appRealm.updateComponent(mapperModel); + + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1"); + GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11"); + GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12"); + + // Add some group mappings directly into LDAP + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm); + + LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak"); + groupMapper.addGroupMappingInLDAP("group1", maryLdap); + groupMapper.addGroupMappingInLDAP("group11", maryLdap); + + // Add some group mapping to model + mary.joinGroup(group12); + + // Assert that mary has both LDAP and DB mapped groups + Set maryGroups = mary.getGroups(); + Assert.assertEquals(3, maryGroups.size()); + Assert.assertTrue(maryGroups.contains(group1)); + Assert.assertTrue(maryGroups.contains(group11)); + Assert.assertTrue(maryGroups.contains(group12)); + + // Assert that access through DB will have just DB mapped groups + System.out.println("******"); + UserModel maryDB = session.userLocalStorage().getUserByUsername("marykeycloak", appRealm); + Set maryDBGroups = maryDB.getGroups(); + Assert.assertFalse(maryDBGroups.contains(group1)); + Assert.assertFalse(maryDBGroups.contains(group11)); + Assert.assertTrue(maryDBGroups.contains(group12)); + + // Check through userProvider + List group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10); + List group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10); + List group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10); + Assert.assertEquals(1, group1Members.size()); + Assert.assertEquals("marykeycloak", group1Members.get(0).getUsername()); + Assert.assertEquals(1, group11Members.size()); + Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername()); + Assert.assertEquals(1, group12Members.size()); + Assert.assertEquals("marykeycloak", group12Members.get(0).getUsername()); + + mary.leaveGroup(group12); + try { + mary.leaveGroup(group1); + Assert.fail("It wasn't expected to successfully delete LDAP group mappings in READ_ONLY mode"); + } catch (ModelException expected) { + } + + // Delete role mappings directly in LDAP + deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group1"); + deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group11"); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_importGroupMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper"); + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.IMPORT.toString()); + appRealm.updateComponent(mapperModel); + + // Add some group mappings directly in LDAP + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm); + + LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak"); + groupMapper.addGroupMappingInLDAP("group11", robLdap); + groupMapper.addGroupMappingInLDAP("group12", robLdap); + + // Get user and check that he has requested groupa from LDAP + UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm); + Set 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)); + + // Delete some group mappings in LDAP and check that it doesn't have any effect and user still has groups + deleteGroupMappingsInLDAP(groupMapper, robLdap, "group11"); + deleteGroupMappingsInLDAP(groupMapper, robLdap, "group12"); + robGroups = rob.getGroups(); + Assert.assertTrue(robGroups.contains(group11)); + Assert.assertTrue(robGroups.contains(group12)); + + // Delete group mappings through model and verifies that user doesn't have them anymore + rob.leaveGroup(group11); + rob.leaveGroup(group12); + robGroups = rob.getGroups(); + Assert.assertEquals(0, robGroups.size()); + } finally { + keycloakRule.stopSession(session, false); + } + } + + + // KEYCLOAK-2682 + @Test + public void test04_groupReferencingNonExistentMember() { + KeycloakSession session = keycloakRule.startSession(); + try { + // Ignoring this test on ActiveDirectory as it's not allowed to have LDAP group referencing nonexistent member. KEYCLOAK-2682 was related to OpenLDAP TODO: Better solution than programmatic... + LDAPConfig config = LDAPTestUtils.getLdapProvider(session, ldapModel).getLdapIdentityStore().getConfig(); + if (config.isActiveDirectory()) { + return; + } + + RealmModel appRealm = session.realms().getRealmByName("test"); + + ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper"); + LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString()); + appRealm.updateComponent(mapperModel); + + // 1 - Add some group to LDAP for testing + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm); + LDAPObject group2 = LDAPTestUtils.createLDAPGroup(session, appRealm, ldapModel, "group2", descriptionAttrName, "group2 - description"); + + // 2 - Add one existing user rob to LDAP group + LDAPObject jamesLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "jameskeycloak"); + LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, jamesLdap, false); + + // 3 - Add non-existing user to LDAP group + LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); + nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent"); + LDAPObject nonExistentLdapUser = new LDAPObject(); + nonExistentLdapUser.setDn(nonExistentDn); + LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, nonExistentLdapUser, true); + + // 4 - Check group members. Just existing user rob should be present + groupMapper.syncDataFromFederationProviderToKeycloak(); + GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(appRealm, "/group2"); + List groupUsers = session.users().getGroupMembers(appRealm, kcGroup2, 0, 5); + Assert.assertEquals(1, groupUsers.size()); + UserModel rob = groupUsers.get(0); + Assert.assertEquals("jameskeycloak", rob.getUsername()); + + } finally { + keycloakRule.stopSession(session, false); + } + } + + private void deleteGroupMappingsInLDAP(GroupLDAPStorageMapper groupMapper, LDAPObject ldapUser, String groupName) { + LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName(groupName); + groupMapper.deleteGroupMappingInLDAP(ldapUser, ldapGroup); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java new file mode 100755 index 0000000000..c054e08cc0 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.OAuth2Constants; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +import javax.ws.rs.core.UriBuilder; +import java.net.URL; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPMultipleAttributesTest { + + protected String APP_SERVER_BASE_URL = "http://localhost:8081"; + protected String LOGIN_URL = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")).build("test").toString(); + + private static LDAPRule ldapRule = new LDAPRule(); + + private static ComponentModel ldapModel = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + ldapModel = appRealm.addComponentModel(model); + + LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel); + LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET); + + // Remove current users and add default users + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", null, "88441"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1"); + + // User for testing duplicating surname and postalCode + LDAPObject bruce = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "bwilson", "Bruce", "Wilson", "bwilson@keycloak.org", "Elm 5", "88441", "77332"); + bruce.setAttribute("sn", new LinkedHashSet<>(Arrays.asList("Wilson", "Schneider"))); + ldapFedProvider.getLdapIdentityStore().update(bruce); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, bruce, "Password1"); + + // Create ldap-portal client + ClientModel ldapClient = KeycloakModelUtils.createClient(appRealm, "ldap-portal"); + ldapClient.addRedirectUri("/ldap-portal"); + ldapClient.addRedirectUri("/ldap-portal/*"); + ldapClient.setManagementUrl("/ldap-portal"); + ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("postalCode", "postal_code", "postal_code", "String", true, "", true, true, true)); + ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("street", "street", "street", "String", true, "", true, true, false)); + ldapClient.addScopeMapping(appRealm.getRole("user")); + ldapClient.setSecret("password"); + + // Deploy ldap-portal client + URL url = getClass().getResource("/ldap/ldap-app-keycloak.json"); + keycloakRule.createApplicationDeployment() + .name("ldap-portal").contextPath("/ldap-portal") + .servletClass(LDAPExampleServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplication(); + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected LoginPage loginPage; + + @Test + public void testModel() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + LDAPTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441"); + + UserModel user = session.users().getUserByUsername("bwilson", appRealm); + Assert.assertEquals("bwilson@keycloak.org", user.getEmail()); + Assert.assertEquals("Bruce", user.getFirstName()); + + // There are 2 lastnames in ldif + Assert.assertTrue("Wilson".equals(user.getLastName()) || "Schneider".equals(user.getLastName())); + + // Actually there are 2 postalCodes + List postalCodes = user.getAttribute("postal_code"); + assertPostalCodes(postalCodes, "88441", "77332"); + List tmp = new LinkedList<>(); + tmp.addAll(postalCodes); + postalCodes = tmp; + postalCodes.remove("77332"); + user.setAttribute("postal_code", postalCodes); + + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("bwilson", appRealm); + List postalCodes = user.getAttribute("postal_code"); + assertPostalCodes(postalCodes, "88441"); + List tmp = new LinkedList<>(); + tmp.addAll(postalCodes); + postalCodes = tmp; + postalCodes.add("77332"); + user.setAttribute("postal_code", postalCodes); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("bwilson", appRealm); + assertPostalCodes(user.getAttribute("postal_code"), "88441", "77332"); + } finally { + keycloakRule.stopSession(session, true); + } + } + + private void assertPostalCodes(List postalCodes, String... expectedPostalCodes) { + if (expectedPostalCodes == null && postalCodes.isEmpty()) { + return; + } + + + Assert.assertEquals(expectedPostalCodes.length, postalCodes.size()); + for (String expected : expectedPostalCodes) { + if (!postalCodes.contains(expected)) { + Assert.fail("postalCode '" + expected + "' not in postalCodes: " + postalCodes); + } + } + } + + @Test + public void ldapPortalEndToEndTest() { + // Login as bwilson + driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + loginPage.login("bwilson", "Password1"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal")); + String pageSource = driver.getPageSource(); + System.out.println(pageSource); + Assert.assertTrue(pageSource.contains("bwilson") && pageSource.contains("Bruce")); + Assert.assertTrue(pageSource.contains("street") && pageSource.contains("Elm 5")); + Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441") && pageSource.contains("77332")); + + // Logout + String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")) + .queryParam(OAuth2Constants.REDIRECT_URI, APP_SERVER_BASE_URL + "/ldap-portal").build("test").toString(); + driver.navigate().to(logoutUri); + + // Login as jbrown + driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + loginPage.login("jbrown", "Password1"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal")); + pageSource = driver.getPageSource(); + System.out.println(pageSource); + Assert.assertTrue(pageSource.contains("jbrown") && pageSource.contains("James Brown")); + Assert.assertFalse(pageSource.contains("street")); + Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441")); + Assert.assertFalse(pageSource.contains("77332")); + + // Logout + driver.navigate().to(logoutUri); + } + + + +} + + diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java index 85f7812f57..13735e6f39 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java @@ -79,16 +79,6 @@ public class LDAPProvidersIntegrationTest { private static ComponentModel ldapModel = null; - private static MultivaluedHashMap getLdapRuleConfig() { - MultivaluedHashMap config = new MultivaluedHashMap<>(); - Map ldapConfig = ldapRule.getConfig(); - for (Map.Entry entry : ldapConfig.entrySet()) { - config.add(entry.getKey(), entry.getValue()); - - } - return config; - - } private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { @@ -96,7 +86,7 @@ public class LDAPProvidersIntegrationTest { public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "marykeycloak", "mary@test.com", "password-app"); - MultivaluedHashMap ldapConfig = getLdapRuleConfig(); + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true"); ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); UserStorageProviderModel model = new UserStorageProviderModel(); @@ -441,7 +431,7 @@ public class LDAPProvidersIntegrationTest { } } - @Test + //@Test // don't think we should support this, bburke public void testDirectLDAPUpdate() { KeycloakSession session = keycloakRule.startSession(); @@ -728,6 +718,17 @@ public class LDAPProvidersIntegrationTest { @Test public void testRemoveFederatedUser() { + /* + { + KeycloakSession session = keycloakRule.startSession(); + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel user = session.users().getUserByUsername("registerUserSuccess2", appRealm); + keycloakRule.stopSession(session, true); + if (user == null) { + registerUserLdapSuccess(); + } + } + */ KeycloakSession session = keycloakRule.startSession(); try { RealmModel appRealm = session.realms().getRealmByName("test"); @@ -756,26 +757,26 @@ public class LDAPProvidersIntegrationTest { LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", null, "124"); // Users are not at local store at this moment - Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm)); - Assert.assertNull(session.userStorage().getUserByUsername("username2", appRealm)); - Assert.assertNull(session.userStorage().getUserByUsername("username3", appRealm)); - Assert.assertNull(session.userStorage().getUserByUsername("username4", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("username1", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("username2", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("username3", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("username4", appRealm)); // search by username session.users().searchForUser("username1", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username1", "John1", "Doel1", "user1@email.org", "121"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username1", "John1", "Doel1", "user1@email.org", "121"); // search by email session.users().searchForUser("user2@email.org", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username2", "John2", "Doel2", "user2@email.org", "122"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username2", "John2", "Doel2", "user2@email.org", "122"); // search by lastName session.users().searchForUser("Doel3", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username3", "John3", "Doel3", "user3@email.org", "123"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username3", "John3", "Doel3", "user3@email.org", "123"); // search by firstName + lastName session.users().searchForUser("John4 Doel4", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username4", "John4", "Doel4", "user4@email.org", "124"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username4", "John4", "Doel4", "user4@email.org", "124"); } finally { keycloakRule.stopSession(session, true); } @@ -803,15 +804,15 @@ public class LDAPProvidersIntegrationTest { LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username7", "John7", "Doel7", "user7@email.org", null, "127"); // search by email - session.users().searchForUser("user5@email.org", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125"); + List list = session.users().searchForUser("user5@email.org", appRealm); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125"); session.users().searchForUser("John6 Doel6", appRealm); - LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126"); session.users().searchForUser("user7@email.org", appRealm); session.users().searchForUser("John7 Doel7", appRealm); - Assert.assertNull(session.userStorage().getUserByUsername("username7", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("username7", appRealm)); // Remove custom filter ldapModel.getConfig().remove(LDAPConstants.CUSTOM_USER_SEARCH_FILTER); @@ -850,7 +851,7 @@ public class LDAPProvidersIntegrationTest { Assert.assertTrue(session.users().removeUser(appRealm, user)); // Assert user not available locally, but will be reimported from LDAP once searched - Assert.assertNull(session.userStorage().getUserByUsername("johnkeycloak", appRealm)); + Assert.assertNull(session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm)); Assert.assertNotNull(session.users().getUserByUsername("johnkeycloak", appRealm)); } finally { keycloakRule.stopSession(session, false); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java new file mode 100644 index 0000000000..19c2b6b9d0 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java @@ -0,0 +1,362 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapper; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +import java.util.Map; +import java.util.Set; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPRoleMappingsTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static ComponentModel ldapModel = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); + + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + + ldapModel = appRealm.addComponentModel(model); + + // Delete all LDAP users + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + // Add sample application + ClientModel finance = appRealm.addClient("finance"); + + // Delete all LDAP roles + LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY); + LDAPTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "realmRolesMapper"); + LDAPTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper"); + + // Add some users for testing + LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); + + LDAPObject mary = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1"); + + LDAPObject rob = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1"); + + // Add some roles for testing + LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1"); + LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2"); + LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1"); + + // Sync LDAP roles to Keycloak DB + LDAPTestUtils.syncRolesFromLDAP(appRealm, ldapFedProvider, ldapModel); + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver driver; + + @WebResource + protected AppPage appPage; + + @WebResource + protected LoginPage loginPage; + + @Test + public void test01_ldapOnlyRoleMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY); + + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + // 1 - Grant some roles in LDAP + + // This role should already exists as it was imported from LDAP + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + john.grantRole(realmRole1); + + // This role should already exists as it was imported from LDAP + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + mary.grantRole(realmRole2); + + // This role may already exists from previous test (was imported from LDAP), but may not + RoleModel realmRole3 = appRealm.getRole("realmRole3"); + if (realmRole3 == null) { + realmRole3 = appRealm.addRole("realmRole3"); + } + + john.grantRole(realmRole3); + mary.grantRole(realmRole3); + + ClientModel accountApp = appRealm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + ClientModel financeApp = appRealm.getClientByClientId("finance"); + + RoleModel manageAccountRole = accountApp.getRole(AccountRoles.MANAGE_ACCOUNT); + RoleModel financeRole1 = financeApp.getRole("financeRole1"); + john.grantRole(financeRole1); + + // 2 - Check that role mappings are not in local Keycloak DB (They are in LDAP). + + UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm); + Set johnDbRoles = johnDb.getRoleMappings(); + Assert.assertFalse(johnDbRoles.contains(realmRole1)); + Assert.assertFalse(johnDbRoles.contains(realmRole2)); + Assert.assertFalse(johnDbRoles.contains(realmRole3)); + Assert.assertFalse(johnDbRoles.contains(financeRole1)); + Assert.assertTrue(johnDbRoles.contains(manageAccountRole)); + + // 3 - Check that role mappings are in LDAP and hence available through federation + + Set johnRoles = john.getRoleMappings(); + Assert.assertTrue(johnRoles.contains(realmRole1)); + Assert.assertFalse(johnRoles.contains(realmRole2)); + Assert.assertTrue(johnRoles.contains(realmRole3)); + Assert.assertTrue(johnRoles.contains(financeRole1)); + Assert.assertTrue(johnRoles.contains(manageAccountRole)); + + Set johnRealmRoles = john.getRealmRoleMappings(); + Assert.assertEquals(2, johnRealmRoles.size()); + Assert.assertTrue(johnRealmRoles.contains(realmRole1)); + Assert.assertTrue(johnRealmRoles.contains(realmRole3)); + + // account roles are not mapped in LDAP. Those are in Keycloak DB + Set johnAccountRoles = john.getClientRoleMappings(accountApp); + Assert.assertTrue(johnAccountRoles.contains(manageAccountRole)); + + Set johnFinanceRoles = john.getClientRoleMappings(financeApp); + Assert.assertEquals(1, johnFinanceRoles.size()); + Assert.assertTrue(johnFinanceRoles.contains(financeRole1)); + + // 4 - Delete some role mappings and check they are deleted + + john.deleteRoleMapping(realmRole3); + john.deleteRoleMapping(realmRole1); + john.deleteRoleMapping(financeRole1); + john.deleteRoleMapping(manageAccountRole); + + johnRoles = john.getRoleMappings(); + Assert.assertFalse(johnRoles.contains(realmRole1)); + Assert.assertFalse(johnRoles.contains(realmRole2)); + Assert.assertFalse(johnRoles.contains(realmRole3)); + Assert.assertFalse(johnRoles.contains(financeRole1)); + Assert.assertFalse(johnRoles.contains(manageAccountRole)); + + // Cleanup + mary.deleteRoleMapping(realmRole2); + mary.deleteRoleMapping(realmRole3); + john.grantRole(manageAccountRole); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_readOnlyRoleMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.READ_ONLY); + + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + RoleModel realmRole3 = appRealm.getRole("realmRole3"); + if (realmRole3 == null) { + realmRole3 = appRealm.addRole("realmRole3"); + } + + // Add some role mappings directly into LDAP + ComponentModel roleMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "realmRolesMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + RoleLDAPStorageMapper roleMapper = LDAPTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm); + + LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak"); + roleMapper.addRoleMappingInLDAP("realmRole1", maryLdap); + roleMapper.addRoleMappingInLDAP("realmRole2", maryLdap); + + // Add some role to model + mary.grantRole(realmRole3); + + // Assert that mary has both LDAP and DB mapped roles + Set maryRoles = mary.getRealmRoleMappings(); + Assert.assertTrue(maryRoles.contains(realmRole1)); + Assert.assertTrue(maryRoles.contains(realmRole2)); + Assert.assertTrue(maryRoles.contains(realmRole3)); + + // Assert that access through DB will have just DB mapped role + UserModel maryDB = session.userLocalStorage().getUserByUsername("marykeycloak", appRealm); + Set maryDBRoles = maryDB.getRealmRoleMappings(); + Assert.assertFalse(maryDBRoles.contains(realmRole1)); + Assert.assertFalse(maryDBRoles.contains(realmRole2)); + Assert.assertTrue(maryDBRoles.contains(realmRole3)); + + mary.deleteRoleMapping(realmRole3); + try { + mary.deleteRoleMapping(realmRole1); + Assert.fail("It wasn't expected to successfully delete LDAP role mappings in READ_ONLY mode"); + } catch (ModelException expected) { + } + + // Delete role mappings directly in LDAP + deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole2"); + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + // Assert role mappings is not available + Set maryRoles = mary.getRealmRoleMappings(); + Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole1"))); + Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole2"))); + Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole3"))); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_importRoleMappings() { + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.IMPORT); + + // Add some role mappings directly in LDAP + ComponentModel roleMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "realmRolesMapper"); + LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + RoleLDAPStorageMapper roleMapper = LDAPTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm); + + LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak"); + roleMapper.addRoleMappingInLDAP("realmRole1", robLdap); + roleMapper.addRoleMappingInLDAP("realmRole2", robLdap); + + // Get user and check that he has requested roles from LDAP + UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm); + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + RoleModel realmRole3 = appRealm.getRole("realmRole3"); + if (realmRole3 == null) { + realmRole3 = appRealm.addRole("realmRole3"); + } + Set robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole1)); + Assert.assertTrue(robRoles.contains(realmRole2)); + Assert.assertFalse(robRoles.contains(realmRole3)); + + // Add some role mappings in model and check that user has it + rob.grantRole(realmRole3); + robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole3)); + + // Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role + deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole2"); + robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole1)); + Assert.assertTrue(robRoles.contains(realmRole2)); + + // Delete role mappings through model and verifies that user doesn't have them anymore + rob.deleteRoleMapping(realmRole1); + rob.deleteRoleMapping(realmRole2); + rob.deleteRoleMapping(realmRole3); + robRoles = rob.getRealmRoleMappings(); + Assert.assertFalse(robRoles.contains(realmRole1)); + Assert.assertFalse(robRoles.contains(realmRole2)); + Assert.assertFalse(robRoles.contains(realmRole3)); + } finally { + keycloakRule.stopSession(session, false); + } + } + + private void deleteRoleMappingsInLDAP(RoleLDAPStorageMapper roleMapper, LDAPObject ldapUser, String roleName) { + LDAPObject ldapRole1 = roleMapper.loadLDAPRoleByName(roleName); + roleMapper.deleteRoleMappingInLDAP(ldapUser, ldapRole1); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java new file mode 100755 index 0000000000..c1d55c1589 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java @@ -0,0 +1,378 @@ +/* + * Copyright 2016 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.federation.storage.ldap; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runners.MethodSorters; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.common.util.Time; +import org.keycloak.component.ComponentModel; +import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.user.SynchronizationResult; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.LDAPRule; + +import java.util.Map; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPSyncTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static UserStorageProviderModel ldapModel = null; + + private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + // Other tests may left Time offset uncleared, which could cause issues + Time.setOffset(0); + + MultivaluedHashMap ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule); + ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "false"); + ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString()); + UserStorageProviderModel model = new UserStorageProviderModel(); + model.setLastSync(0); + model.setChangedSyncPeriod(-1); + model.setFullSyncPeriod(-1); + model.setName("test-ldap"); + model.setPriority(0); + model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + model.setConfig(ldapConfig); + + ldapModel = new UserStorageProviderModel(appRealm.addComponentModel(model)); + + + LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel); + + // Delete all LDAP users and add 5 new users for testing + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + for (int i=1 ; i<=5 ; i++) { + LDAPObject ldapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, ldapUser, "Password1"); + } + } + }); + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + +// @Test +// public void test01runit() throws Exception { +// Thread.sleep(10000000); +// } + + @Test + public void test01LDAPSync() { + UserStorageSyncManager usersSyncManager = new UserStorageSyncManager(); + + // wait a bit + sleep(ldapRule.getSleepTime()); + + KeycloakSession session = keycloakRule.startSession(); + try { + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + SynchronizationResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel); + LDAPTestUtils.assertSyncEquals(syncResult, 5, 0, 0, 0); + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + UserProvider userProvider = session.userLocalStorage(); + // Assert users imported + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121"); + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122"); + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user3", "User3FN", "User3LN", "user3@email.org", "123"); + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user4", "User4FN", "User4LN", "user4@email.org", "124"); + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org", "125"); + + // Assert lastSync time updated + Assert.assertTrue(ldapModel.getLastSync() > 0); + for (UserStorageProviderModel persistentFedModel : testRealm.getUserStorageProviders()) { + if (LDAPStorageProviderFactory.PROVIDER_NAME.equals(persistentFedModel.getProviderId())) { + Assert.assertTrue(persistentFedModel.getLastSync() > 0); + } else { + // Dummy provider has still 0 + Assert.assertEquals(0, persistentFedModel.getLastSync()); + } + } + + // wait a bit + sleep(ldapRule.getSleepTime()); + + // Add user to LDAP and update 'user5' in LDAP + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", null, "126"); + LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5"); + // NOTE: Changing LDAP attributes directly here + ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org"); + ldapUser5.setSingleAttribute(LDAPConstants.POSTAL_CODE, "521"); + ldapFedProvider.getLdapIdentityStore().update(ldapUser5); + + // Assert still old users in local provider + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org", "125"); + Assert.assertNull(userProvider.getUserByUsername("user6", testRealm)); + + // Trigger partial sync + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + SynchronizationResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel); + LDAPTestUtils.assertSyncEquals(syncResult, 1, 1, 0, 0); + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + UserProvider userProvider = session.userLocalStorage(); + // Assert users updated in local provider + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5updated@email.org", "521"); + LDAPTestUtils.assertUserImported(userProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126"); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02duplicateUsernameAndEmailSync() { + LDAPObject duplicatedLdapUser; + + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + LDAPTestUtils.addLocalUser(session, testRealm, "user7", "user7@email.org", "password"); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + + // Add user to LDAP with duplicated username "user7" + duplicatedLdapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7", "User7FN", "User7LN", "user7-something@email.org", null, "126"); + + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Assert syncing from LDAP fails due to duplicated username + SynchronizationResult result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel); + Assert.assertEquals(1, result.getFailed()); + + // Remove "user7" from LDAP + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + ldapFedProvider.getLdapIdentityStore().remove(duplicatedLdapUser); + + // Add user to LDAP with duplicated email "user7@email.org" + duplicatedLdapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7-something", "User7FNN", "User7LNL", "user7@email.org", null, "126"); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Assert syncing from LDAP fails due to duplicated email + SynchronizationResult result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel); + Assert.assertEquals(1, result.getFailed()); + Assert.assertNull(session.userLocalStorage().getUserByUsername("user7-something", testRealm)); + + // Update LDAP user to avoid duplicated email + duplicatedLdapUser.setSingleAttribute(LDAPConstants.EMAIL, "user7-changed@email.org"); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + ldapFedProvider.getLdapIdentityStore().update(duplicatedLdapUser); + + // Assert user successfully synced now + result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel); + Assert.assertEquals(0, result.getFailed()); + } finally { + keycloakRule.stopSession(session, true); + } + + // Assert user imported in another transaction + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + LDAPTestUtils.assertUserImported(session.userLocalStorage(), testRealm, "user7-something", "User7FNN", "User7LNL", "user7-changed@email.org", "126"); + } finally { + keycloakRule.stopSession(session, false); + } + } + + // KEYCLOAK-1571 + @Test + public void test03SameUUIDAndUsernameSync() { + KeycloakSession session = keycloakRule.startSession(); + String origUuidAttrName; + + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Remove all users from model + for (UserModel user : session.userLocalStorage().getUsers(testRealm, true)) { + session.userLocalStorage().removeUser(testRealm, user); + } + + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + + // Change name of UUID attribute to same like usernameAttribute + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + String uidAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().getUsernameLdapAttribute(); + origUuidAttrName = providerModel.getConfig().getFirst(LDAPConstants.UUID_LDAP_ATTRIBUTE); + providerModel.getConfig().putSingle(LDAPConstants.UUID_LDAP_ATTRIBUTE, uidAttrName); + + // Need to change this due to ApacheDS pagination bug (For other LDAP servers, pagination works fine) TODO: Remove once ApacheDS upgraded and pagination is fixed + providerModel.getConfig().putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "10"); + testRealm.updateComponent(providerModel); + + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + SynchronizationResult syncResult = new UserStorageSyncManager().syncAllUsers(sessionFactory, "test", providerModel); + Assert.assertEquals(0, syncResult.getFailed()); + + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Assert users imported with correct LDAP_ID + LDAPTestUtils.assertUserImported(session.users(), testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121"); + LDAPTestUtils.assertUserImported(session.users(), testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122"); + UserModel user1 = session.users().getUserByUsername("user1", testRealm); + Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID)); + + // Revert config changes + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + providerModel.getConfig().putSingle(LDAPConstants.UUID_LDAP_ATTRIBUTE, origUuidAttrName); + testRealm.updateComponent(providerModel); + } finally { + keycloakRule.stopSession(session, true); + } + } + + // KEYCLOAK-1728 + @Test + public void test04MissingLDAPUsernameSync() { + KeycloakSession session = keycloakRule.startSession(); + String origUsernameAttrName; + + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Remove all users from model + for (UserModel user : session.userLocalStorage().getUsers(testRealm, true)) { + System.out.println("trying to delete user: " + user.getUsername()); + session.getUserCache().evict(testRealm, user); + session.userLocalStorage().removeUser(testRealm, user); + } + + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + + // Add street mapper and add some user including street + ComponentModel streetMapper = LDAPTestUtils.addUserAttributeMapper(testRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel); + LDAPObject streetUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user8", "User8FN", "User8LN", "user8@email.org", "user8street", "126"); + + // Change name of username attribute name to street + origUsernameAttrName = providerModel.getConfig().getFirst(LDAPConstants.USERNAME_LDAP_ATTRIBUTE); + providerModel.getConfig().putSingle(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "street"); + + // Need to change this due to ApacheDS pagination bug (For other LDAP servers, pagination works fine) TODO: Remove once ApacheDS upgraded and pagination is fixed + providerModel.getConfig().putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "10"); + testRealm.updateComponent(providerModel); + + } finally { + keycloakRule.stopSession(session, true); + } + + // Just user8 synced. All others failed to sync + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + SynchronizationResult syncResult = new UserStorageSyncManager().syncAllUsers(sessionFactory, "test", providerModel); + Assert.assertEquals(1, syncResult.getAdded()); + Assert.assertTrue(syncResult.getFailed() > 0); + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Revert config changes + UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm); + providerModel.getConfig().putSingle(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, origUsernameAttrName); + testRealm.updateComponent(providerModel); + ComponentModel streetMapper = LDAPTestUtils.getComponentByName(testRealm, providerModel, "streetMapper"); + testRealm.removeComponent(streetMapper); + } finally { + keycloakRule.stopSession(session, true); + } + } + + private void sleep(int time) { + try { + Thread.sleep(time); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java index f4bf1823c0..4b612f2f26 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.federation.storage.ldap; import org.junit.Assert; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; @@ -46,6 +47,7 @@ import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory; import org.keycloak.storage.ldap.mappers.membership.role.RoleMapperConfig; import org.keycloak.storage.user.SynchronizationResult; +import org.keycloak.testsuite.rule.LDAPRule; import java.util.Arrays; import java.util.Collections; @@ -58,9 +60,20 @@ import java.util.Set; * @author Marek Posolda */ public class LDAPTestUtils { + public static MultivaluedHashMap getLdapRuleConfig(LDAPRule ldapRule) { + MultivaluedHashMap config = new MultivaluedHashMap<>(); + Map ldapConfig = ldapRule.getConfig(); + for (Map.Entry entry : ldapConfig.entrySet()) { + config.add(entry.getKey(), entry.getValue()); + + } + return config; + + } + public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) { - UserModel user = session.userStorage().addUser(realm, username); + UserModel user = session.userLocalStorage().addUser(realm, username); user.setEmail(email); user.setEnabled(true);