diff --git a/core/src/main/java/org/keycloak/representations/idm/StorageProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/StorageProviderRepresentation.java
deleted file mode 100755
index 04bf1746364..00000000000
--- a/core/src/main/java/org/keycloak/representations/idm/StorageProviderRepresentation.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.representations.idm;
-
-import java.util.Map;
-
-/**
- * @author Marek Posolda
- */
-public class StorageProviderRepresentation {
-
- private String id;
- private String displayName;
- private String providerName;
- private Map config;
- private int priority;
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getDisplayName() {
- return displayName;
- }
-
- public void setDisplayName(String displayName) {
- this.displayName = displayName;
- }
-
- public String getProviderName() {
- return providerName;
- }
-
- public void setProviderName(String providerName) {
- this.providerName = providerName;
- }
-
-
- public Map getConfig() {
- return config;
- }
-
- public void setConfig(Map config) {
- this.config = config;
- }
-
- public int getPriority() {
- return priority;
- }
-
- public void setPriority(int priority) {
- this.priority = priority;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- StorageProviderRepresentation that = (StorageProviderRepresentation) o;
-
- if (!id.equals(that.id)) return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-}
diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
index 7e3d6e70a2c..ed6c4950148 100644
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
@@ -18,6 +18,7 @@
package org.keycloak.federation.kerberos;
import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.component.ComponentModel;
import org.keycloak.models.UserFederationProviderModel;
import java.util.Map;
@@ -29,31 +30,41 @@ import java.util.Map;
*/
public abstract class CommonKerberosConfig {
- private final UserFederationProviderModel providerModel;
+ protected UserFederationProviderModel providerModel;
+ protected ComponentModel componentModel;
public CommonKerberosConfig(UserFederationProviderModel userFederationProvider) {
this.providerModel = userFederationProvider;
}
+ public CommonKerberosConfig(ComponentModel componentModel) {
+ this.componentModel = componentModel;
+ }
+
// Should be always true for KerberosFederationProvider
public boolean isAllowKerberosAuthentication() {
- return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
+ if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
+ else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
}
public String getKerberosRealm() {
- return getConfig().get(KerberosConstants.KERBEROS_REALM);
+ if (providerModel != null) return getConfig().get(KerberosConstants.KERBEROS_REALM);
+ else return componentModel.getConfig().getFirst(KerberosConstants.KERBEROS_REALM);
}
public String getServerPrincipal() {
- return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
+ if (providerModel != null) return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
+ else return componentModel.getConfig().getFirst(KerberosConstants.SERVER_PRINCIPAL);
}
public String getKeyTab() {
- return getConfig().get(KerberosConstants.KEYTAB);
+ if (providerModel != null) return getConfig().get(KerberosConstants.KEYTAB);
+ else return componentModel.getConfig().getFirst(KerberosConstants.KEYTAB);
}
public boolean isDebug() {
- return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
+ if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
+ else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.DEBUG));
}
protected Map getConfig() {
diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/GroupTreeResolverTest.java
similarity index 100%
rename from federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/GroupTreeResolverTest.java
rename to federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/GroupTreeResolverTest.java
diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java
similarity index 100%
rename from federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java
rename to federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java
diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPMappersComparatorTest.java b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPMappersComparatorTest.java
similarity index 100%
rename from federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPMappersComparatorTest.java
rename to federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPMappersComparatorTest.java
diff --git a/federation/ldap2/pom.xml b/federation/ldap2/pom.xml
new file mode 100755
index 00000000000..dccdee0233a
--- /dev/null
+++ b/federation/ldap2/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+
+ keycloak-parent
+ org.keycloak
+ 2.4.0.CR1-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ keycloak-ldap-storage
+ Keycloak LDAP UserStoreProvider
+
+
+
+ 1.8
+ 1.8
+
+
+
+
+ org.keycloak
+ keycloak-core
+ provided
+
+
+ org.keycloak
+ keycloak-server-spi
+ provided
+
+
+ org.keycloak
+ keycloak-kerberos-federation
+ provided
+
+
+ org.jboss.resteasy
+ resteasy-jaxrs
+ provided
+
+
+ log4j
+ log4j
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ slf4j-simple
+
+
+
+
+ org.jboss.logging
+ jboss-logging
+ provided
+
+
+ junit
+ junit
+ test
+
+
+ org.jboss.spec.javax.transaction
+ jboss-transaction-api_1.2_spec
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+
+
+
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
new file mode 100644
index 00000000000..e5d497b2e06
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
@@ -0,0 +1,182 @@
+/*
+ * 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.storage.ldap;
+
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.models.LDAPConstants;
+
+import javax.naming.directory.SearchControls;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * @author Marek Posolda
+ *
+ */
+public class LDAPConfig {
+
+ private final MultivaluedHashMap config;
+
+ public LDAPConfig(MultivaluedHashMap config) {
+ this.config = config;
+ }
+
+ public String getConnectionUrl() {
+ return config.getFirst(LDAPConstants.CONNECTION_URL);
+ }
+
+ public String getFactoryName() {
+ // hardcoded for now
+ return "com.sun.jndi.ldap.LdapCtxFactory";
+ }
+
+ public String getAuthType() {
+ String value = config.getFirst(LDAPConstants.AUTH_TYPE);
+ if (value == null) {
+ return LDAPConstants.AUTH_TYPE_SIMPLE;
+ } else {
+ return value;
+ }
+ }
+
+ public String getUseTruststoreSpi() {
+ return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
+ }
+
+ public String getUsersDn() {
+ String usersDn = config.getFirst(LDAPConstants.USERS_DN);
+
+ if (usersDn == null) {
+ // Just for the backwards compatibility 1.2 -> 1.3 . Should be removed later.
+ usersDn = config.getFirst("userDnSuffix");
+ }
+
+ return usersDn;
+ }
+
+ public Collection getUserObjectClasses() {
+ String objClassesCfg = config.getFirst(LDAPConstants.USER_OBJECT_CLASSES);
+ String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson,organizationalPerson";
+
+ String[] objectClasses = objClassesStr.split(",");
+
+ // Trim them
+ Set userObjClasses = new HashSet<>();
+ for (int i=0 ; i 1.3 . Should be removed later.
+ rdn = LDAPConstants.CN;
+ }
+
+ }
+ return rdn;
+ }
+
+
+ public String getCustomUserSearchFilter() {
+ String customFilter = config.getFirst(LDAPConstants.CUSTOM_USER_SEARCH_FILTER);
+ if (customFilter != null) {
+ customFilter = customFilter.trim();
+ if (customFilter.length() > 0) {
+ return customFilter;
+ }
+ }
+ return null;
+ }
+
+ public LDAPStorageProviderFactory.EditMode getEditMode() {
+ String editModeString = config.getFirst(LDAPConstants.EDIT_MODE);
+ if (editModeString == null) {
+ return LDAPStorageProviderFactory.EditMode.READ_ONLY;
+ } else {
+ return LDAPStorageProviderFactory.EditMode.valueOf(editModeString);
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
new file mode 100644
index 00000000000..7dc3086ba4b
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
@@ -0,0 +1,95 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Marek Posolda
+ */
+public class LDAPIdentityStoreRegistry {
+
+ private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
+
+ private Map ldapStores = new ConcurrentHashMap();
+
+ public LDAPIdentityStore getLdapStore(ComponentModel model) {
+ LDAPIdentityStoreContext context = ldapStores.get(model.getId());
+
+ // Ldap config might have changed for the realm. In this case, we must re-initialize
+ MultivaluedHashMap config = model.getConfig();
+ if (context == null || !config.equals(context.config)) {
+ logLDAPConfig(model.getName(), config);
+
+ LDAPIdentityStore store = createLdapIdentityStore(config);
+ context = new LDAPIdentityStoreContext(config, store);
+ ldapStores.put(model.getId(), context);
+ }
+ return context.store;
+ }
+
+ // Don't log LDAP password
+ private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap ldapConfig) {
+ MultivaluedHashMap copy = new MultivaluedHashMap(ldapConfig);
+ copy.remove(LDAPConstants.BIND_CREDENTIAL);
+ logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
+ }
+
+ /**
+ * @param ldapConfig from realm
+ * @return PartitionManager instance based on LDAP store
+ */
+ public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap ldapConfig) {
+ LDAPConfig cfg = new LDAPConfig(ldapConfig);
+
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.prefsize", "5");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.timeout", "300000");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
+
+ return new LDAPIdentityStore(cfg);
+ }
+
+ private static void checkSystemProperty(String name, String defaultValue) {
+ if (System.getProperty(name) == null) {
+ System.setProperty(name, defaultValue);
+ }
+ }
+
+
+ private class LDAPIdentityStoreContext {
+
+ private LDAPIdentityStoreContext(MultivaluedHashMap config, LDAPIdentityStore store) {
+ this.config = config;
+ this.store = store;
+ }
+
+ private MultivaluedHashMap config;
+ private LDAPIdentityStore store;
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
new file mode 100755
index 00000000000..272dfe5ecee
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
@@ -0,0 +1,645 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.credential.CredentialAuthentication;
+import org.keycloak.credential.CredentialInput;
+import org.keycloak.credential.CredentialInputUpdater;
+import org.keycloak.credential.CredentialInputValidator;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
+import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
+import org.keycloak.models.CredentialValidationOutput;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.ModelReadOnlyException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.UserManager;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
+import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.PasswordUpdated;
+import org.keycloak.storage.user.ImportedUserValidation;
+import org.keycloak.storage.user.UserLookupProvider;
+import org.keycloak.storage.user.UserQueryProvider;
+import org.keycloak.storage.user.UserRegistrationProvider;
+
+import javax.naming.AuthenticationException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Marek Posolda
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class LDAPStorageProvider implements UserStorageProvider,
+ CredentialInputValidator,
+ CredentialInputUpdater,
+ CredentialAuthentication,
+ UserLookupProvider,
+ UserRegistrationProvider,
+ UserQueryProvider,
+ ImportedUserValidation {
+ private static final Logger logger = Logger.getLogger(LDAPStorageProvider.class);
+
+ protected LDAPStorageProviderFactory factory;
+ protected KeycloakSession session;
+ protected ComponentModel model;
+ protected LDAPIdentityStore ldapIdentityStore;
+ protected LDAPStorageProviderFactory.EditMode editMode;
+ protected LDAPProviderKerberosConfig kerberosConfig;
+ protected PasswordUpdated updater;
+
+ protected final Set supportedCredentialTypes = new HashSet<>();
+
+ public LDAPStorageProvider(LDAPStorageProviderFactory factory, KeycloakSession session, ComponentModel model, LDAPIdentityStore ldapIdentityStore) {
+ this.factory = factory;
+ this.session = session;
+ this.model = model;
+ this.ldapIdentityStore = ldapIdentityStore;
+ this.kerberosConfig = new LDAPProviderKerberosConfig(model);
+ this.editMode = ldapIdentityStore.getConfig().getEditMode();
+
+ supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
+ if (kerberosConfig.isAllowKerberosAuthentication()) {
+ supportedCredentialTypes.add(UserCredentialModel.KERBEROS);
+ }
+ }
+
+ public void setUpdater(PasswordUpdated updater) {
+ this.updater = updater;
+ }
+
+ public KeycloakSession getSession() {
+ return session;
+ }
+
+ public LDAPIdentityStore getLdapIdentityStore() {
+ return this.ldapIdentityStore;
+ }
+
+ public LDAPStorageProviderFactory.EditMode getEditMode() {
+ return editMode;
+ }
+
+ public ComponentModel getModel() {
+ return model;
+ }
+
+ @Override
+ public UserModel validate(RealmModel realm, UserModel local) {
+ LDAPObject ldapObject = loadAndValidateUser(realm, local);
+ if (ldapObject == null) {
+ return null;
+ }
+
+ return proxy(realm, local, ldapObject);
+ }
+
+ protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject) {
+ UserModel proxied = local;
+ switch (editMode) {
+ case READ_ONLY:
+ proxied = new ReadonlyLDAPUserModelDelegate(local, this);
+ break;
+ case WRITABLE:
+ proxied = new WritableLDAPUserModelDelegate(local, this, ldapObject);
+ break;
+ case UNSYNCED:
+ proxied = new UnsyncedLDAPUserModelDelegate(local, this);
+ }
+
+ List mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = sortMappersAsc(mappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied, realm);
+ }
+
+ return proxied;
+ }
+
+ @Override
+ public boolean supportsCredentialAuthenticationFor(String type) {
+ return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
+ }
+
+ @Override
+ public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public void grantToAllUsers(RealmModel realm, RoleModel role) {
+
+ }
+
+ public boolean synchronizeRegistrations() {
+ return "true".equalsIgnoreCase(model.getConfig().getFirst(LDAPConstants.SYNC_REGISTRATIONS)) && editMode == LDAPStorageProviderFactory.EditMode.WRITABLE;
+ }
+
+ @Override
+ public UserModel addUser(RealmModel realm, String username) {
+ if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY || editMode == LDAPStorageProviderFactory.EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");
+ if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
+ UserModel user = session.userLocalStorage().addUser(realm, username);
+ user.setFederationLink(model.getId());
+ LDAPObject ldapUser = LDAPUtils.addUserToLDAP(this, realm, user);
+ LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
+ user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
+ user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getDn().toString());
+
+ return proxy(realm, user, ldapUser);
+ }
+
+ @Override
+ public boolean removeUser(RealmModel realm, UserModel user) {
+ if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY || editMode == LDAPStorageProviderFactory.EditMode.UNSYNCED) {
+ logger.warnf("User '%s' can't be deleted in LDAP as editMode is '%s'. Deleting user just from Keycloak DB, but he will be re-imported from LDAP again once searched in Keycloak", user.getUsername(), editMode.toString());
+ return true;
+ }
+
+ LDAPObject ldapObject = loadAndValidateUser(realm, user);
+ if (ldapObject == null) {
+ logger.warnf("User '%s' can't be deleted from LDAP as it doesn't exist here", user.getUsername());
+ return false;
+ }
+
+ ldapIdentityStore.remove(ldapObject);
+ return true;
+ }
+
+ @Override
+ public UserModel getUserById(String id, RealmModel realm) {
+ StorageId storageId = new StorageId(id);
+ return getUserByUsername(storageId.getExternalId(), realm);
+ }
+
+ @Override
+ public int getUsersCount(RealmModel realm) {
+ return 0;
+ }
+
+ @Override
+ public List getUsers(RealmModel realm) {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List getUsers(RealmModel realm, int firstResult, int maxResults) {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List searchForUser(String search, RealmModel realm) {
+ return searchForUser(search, realm, 0, Integer.MAX_VALUE - 1);
+ }
+
+ @Override
+ public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
+ Map attributes = new HashMap();
+ int spaceIndex = search.lastIndexOf(' ');
+ if (spaceIndex > -1) {
+ String firstName = search.substring(0, spaceIndex).trim();
+ String lastName = search.substring(spaceIndex).trim();
+ attributes.put(UserModel.FIRST_NAME, firstName);
+ attributes.put(UserModel.LAST_NAME, lastName);
+ } else if (search.indexOf('@') > -1) {
+ attributes.put(UserModel.USERNAME, search.trim().toLowerCase());
+ attributes.put(UserModel.EMAIL, search.trim().toLowerCase());
+ } else {
+ attributes.put(UserModel.LAST_NAME, search.trim());
+ attributes.put(UserModel.USERNAME, search.trim().toLowerCase());
+ }
+ return searchForUser(attributes, realm, firstResult, maxResults);
+ }
+
+ @Override
+ public List searchForUser(Map params, RealmModel realm) {
+ return searchForUser(params, realm, 0, Integer.MAX_VALUE - 1);
+ }
+
+ @Override
+ public List searchForUser(Map params, RealmModel realm, int firstResult, int maxResults) {
+ List searchResults =new LinkedList();
+
+ List ldapUsers = searchLDAP(realm, params, maxResults + firstResult);
+ int counter = 0;
+ for (LDAPObject ldapUser : ldapUsers) {
+ if (counter++ < firstResult) continue;
+ String ldapUsername = LDAPUtils.getUsername(ldapUser, this.ldapIdentityStore.getConfig());
+ if (session.userLocalStorage().getUserByUsername(ldapUsername, realm) == null) {
+ UserModel imported = importUserFromLDAP(session, realm, ldapUser);
+ searchResults.add(imported);
+ }
+ }
+
+ return searchResults;
+ }
+
+ @Override
+ public List getGroupMembers(RealmModel realm, GroupModel group) {
+ return getGroupMembers(realm, group, 0, Integer.MAX_VALUE - 1);
+ }
+
+ @Override
+ public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
+ List mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = sortMappersAsc(mappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ List users = ldapMapper.getGroupMembers(mapperModel, this, realm, group, firstResult, maxResults);
+
+ // Sufficient for now
+ if (users.size() > 0) {
+ return users;
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ public List loadUsersByUsernames(List usernames, RealmModel realm) {
+ List result = new ArrayList<>();
+ for (String username : usernames) {
+ UserModel kcUser = session.users().getUserByUsername(username, realm);
+ if (kcUser == null) {
+ logger.warnf("User '%s' referenced by membership wasn't found in LDAP", username);
+ } else if (!model.getId().equals(kcUser.getFederationLink())) {
+ logger.warnf("Incorrect federation provider of user '%s'", kcUser.getUsername());
+ } else {
+ result.add(kcUser);
+ }
+ }
+ return result;
+ }
+
+ protected List searchLDAP(RealmModel realm, Map attributes, int maxResults) {
+
+ List results = new ArrayList();
+ if (attributes.containsKey(UserModel.USERNAME)) {
+ LDAPObject user = loadLDAPUserByUsername(realm, attributes.get(UserModel.USERNAME));
+ if (user != null) {
+ results.add(user);
+ }
+ }
+
+ if (attributes.containsKey(UserModel.EMAIL)) {
+ LDAPObject user = queryByEmail(realm, attributes.get(UserModel.EMAIL));
+ if (user != null) {
+ results.add(user);
+ }
+ }
+
+ if (attributes.containsKey(UserModel.FIRST_NAME) || attributes.containsKey(UserModel.LAST_NAME)) {
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+
+ // Mapper should replace parameter with correct LDAP mapped attributes
+ if (attributes.containsKey(UserModel.FIRST_NAME)) {
+ ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME)));
+ }
+ if (attributes.containsKey(UserModel.LAST_NAME)) {
+ ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME)));
+ }
+
+ List ldapObjects = ldapQuery.getResultList();
+ results.addAll(ldapObjects);
+ }
+
+ return results;
+ }
+
+ /**
+ * @param local
+ * @return ldapUser corresponding to local user or null if user is no longer in LDAP
+ */
+ protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) {
+ LDAPObject ldapUser = loadLDAPUserByUsername(realm, local.getUsername());
+ if (ldapUser == null) {
+ return null;
+ }
+ LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
+
+ if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) {
+ return ldapUser;
+ } else {
+ logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getFirstAttribute(LDAPConstants.LDAP_ID));
+ return null;
+ }
+ }
+
+ @Override
+ public UserModel getUserByUsername(String username, RealmModel realm) {
+ LDAPObject ldapUser = loadLDAPUserByUsername(realm, username);
+ if (ldapUser == null) {
+ return null;
+ }
+
+ return importUserFromLDAP(session, realm, ldapUser);
+ }
+
+ protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) {
+ String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
+ LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
+
+ UserModel imported = session.userLocalStorage().addUser(realm, ldapUsername);
+ imported.setEnabled(true);
+
+ List mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = sortMappersDesc(mappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
+ }
+ LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, imported, realm, true);
+ }
+
+ String userDN = ldapUser.getDn().toString();
+ imported.setFederationLink(model.getId());
+ imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
+ imported.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
+
+ logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
+ ldapUser.getUuid(), userDN);
+ return proxy(realm, imported, ldapUser);
+ }
+
+ protected LDAPObject queryByEmail(RealmModel realm, String email) {
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+
+ // Mapper should replace "email" in parameter name with correct LDAP mapped attribute
+ Condition emailCondition = conditionsBuilder.equal(UserModel.EMAIL, email);
+ ldapQuery.addWhereCondition(emailCondition);
+
+ return ldapQuery.getFirstResult();
+ }
+
+
+ @Override
+ public UserModel getUserByEmail(String email, RealmModel realm) {
+ LDAPObject ldapUser = queryByEmail(realm, email);
+ if (ldapUser == null) {
+ return null;
+ }
+
+ // Check here if user already exists
+ String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
+ if (session.userLocalStorage().getUserByUsername(ldapUsername, realm) != null) {
+ throw new ModelDuplicateException("User with username '" + ldapUsername + "' already exists in Keycloak. It conflicts with LDAP user with email '" + email + "'");
+ }
+
+ return importUserFromLDAP(session, realm, ldapUser);
+ }
+
+ @Override
+ public void preRemove(RealmModel realm) {
+ // complete Don't think we have to do anything
+ }
+
+ @Override
+ public void preRemove(RealmModel realm, RoleModel role) {
+ // TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper?
+ }
+
+ @Override
+ public void preRemove(RealmModel realm, GroupModel group) {
+
+ }
+
+ public boolean validPassword(RealmModel realm, UserModel user, String password) {
+ if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) {
+ // Use Kerberos JAAS (Krb5LoginModule)
+ KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig);
+ return authenticator.validUser(user.getUsername(), password);
+ } else {
+ // Use Naming LDAP API
+ LDAPObject ldapUser = loadAndValidateUser(realm, user);
+
+ try {
+ ldapIdentityStore.validatePassword(ldapUser, password);
+ return true;
+ } catch (AuthenticationException ae) {
+ boolean processed = false;
+ List mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = sortMappersDesc(mappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
+ }
+ LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm);
+ }
+ return processed;
+ }
+ }
+ }
+
+
+ @Override
+ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
+ if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof UserCredentialModel)) return false;
+ if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY) {
+ throw new ModelReadOnlyException("Federated storage is not writable");
+
+ } else if (editMode == LDAPStorageProviderFactory.EditMode.WRITABLE) {
+ LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
+ UserCredentialModel cred = (UserCredentialModel)input;
+ String password = cred.getValue();
+ LDAPObject ldapUser = loadAndValidateUser(realm, user);
+ ldapIdentityStore.updatePassword(ldapUser, password);
+ if (updater != null) updater.passwordUpdated(user, ldapUser, input);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
+
+ }
+
+ @Override
+ public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) {
+ return Collections.EMPTY_SET;
+ }
+
+ public Set getSupportedCredentialTypes() {
+ return new HashSet(this.supportedCredentialTypes);
+ }
+
+
+ @Override
+ public boolean supportsCredentialType(String credentialType) {
+ return getSupportedCredentialTypes().contains(credentialType);
+ }
+
+ @Override
+ public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
+ return getSupportedCredentialTypes().contains(credentialType);
+ }
+
+ @Override
+ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
+ if (!(input instanceof UserCredentialModel)) return false;
+ if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) {
+ return validPassword(realm, user, ((UserCredentialModel)input).getValue());
+ } else {
+ return false; // invalid cred type
+ }
+ }
+
+ @Override
+ public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput cred) {
+ if (!(cred instanceof UserCredentialModel)) CredentialValidationOutput.failed();
+ UserCredentialModel credential = (UserCredentialModel)cred;
+ if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
+ if (kerberosConfig.isAllowKerberosAuthentication()) {
+ String spnegoToken = credential.getValue();
+ SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
+
+ spnegoAuthenticator.authenticate();
+
+ Map state = new HashMap();
+ if (spnegoAuthenticator.isAuthenticated()) {
+
+ // TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG".
+ // Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different)
+ String username = spnegoAuthenticator.getAuthenticatedUsername();
+ UserModel user = findOrCreateAuthenticatedUser(realm, username);
+
+ if (user == null) {
+ logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getName());
+ return CredentialValidationOutput.failed();
+ } else {
+ String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
+ if (delegationCredential != null) {
+ state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential);
+ }
+
+ return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state);
+ }
+ } else {
+ state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
+ return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
+ }
+ }
+ }
+
+ return CredentialValidationOutput.failed();
+ }
+
+ @Override
+ public void close() {
+ }
+
+ /**
+ * Called after successful kerberos authentication
+ *
+ * @param realm realm
+ * @param username username without realm prefix
+ * @return finded or newly created user
+ */
+ protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) {
+ UserModel user = session.userLocalStorage().getUserByUsername(username, realm);
+ if (user != null) {
+ logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", username);
+ if (!model.getId().equals(user.getFederationLink())) {
+ logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]", username, model.getName());
+ return null;
+ } else {
+ LDAPObject ldapObject = loadAndValidateUser(realm, user);
+ if (ldapObject != null) {
+ return proxy(realm, user, ldapObject);
+ } else {
+ logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
+ username, model.getName(), user.getFirstAttribute(LDAPConstants.LDAP_ID));
+ logger.warn("Will re-create user");
+ session.getUserCache().evict(realm, user);
+ new UserManager(session).removeUser(realm, user, session.userLocalStorage());
+ }
+ }
+ }
+
+ // Creating user to local storage
+ logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", username);
+ return getUserByUsername(username, realm);
+ }
+
+ public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+
+ String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();
+ Condition usernameCondition = conditionsBuilder.equal(usernameMappedAttribute, username);
+ ldapQuery.addWhereCondition(usernameCondition);
+
+ LDAPObject ldapUser = ldapQuery.getFirstResult();
+ if (ldapUser == null) {
+ return null;
+ }
+
+ return ldapUser;
+ }
+
+ public LDAPStorageMapper getMapper(ComponentModel mapperModel) {
+ LDAPStorageMapper ldapMapper = (LDAPStorageMapper) getSession().getProvider(LDAPStorageMapper.class, mapperModel);
+ if (ldapMapper == null) {
+ throw new ModelException("Can't find mapper type with ID: " + mapperModel.getProviderId());
+ }
+
+ return ldapMapper;
+ }
+
+
+ public List sortMappersAsc(Collection mappers) {
+ return LDAPMappersComparator.sortAsc(getLdapIdentityStore().getConfig(), mappers);
+ }
+
+ protected List sortMappersDesc(Collection mappers) {
+ return LDAPMappersComparator.sortDesc(getLdapIdentityStore().getConfig(), mappers);
+ }
+
+
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
new file mode 100755
index 00000000000..2ffc16f7b11
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -0,0 +1,437 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.federation.kerberos.CommonKerberosConfig;
+import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator;
+import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
+import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+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.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderFactory;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author Marek Posolda
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class LDAPStorageProviderFactory implements UserStorageProviderFactory, ImportSynchronization {
+
+ /**
+ * Optional type that can be by implementations to describe edit mode of federation storage
+ *
+ */
+ public enum EditMode {
+ /**
+ * federation storage is read-only
+ */
+ READ_ONLY,
+ /**
+ * federation storage is writable
+ *
+ */
+ WRITABLE,
+ /**
+ * updates to user are stored locally and not synced with federation storage.
+ *
+ */
+ UNSYNCED
+ }
+
+
+ private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class);
+ public static final String PROVIDER_NAME = "ldap2";//LDAPConstants.LDAP_PROVIDER;
+
+ private LDAPIdentityStoreRegistry ldapStoreRegistry;
+
+ @Override
+ public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
+ LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
+ return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+ LDAPConfig cfg = new LDAPConfig(config.getConfig());
+ String customFilter = cfg.getCustomUserSearchFilter();
+ LDAPUtils.validateCustomLdapFilter(customFilter);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
+ }
+
+ @Override
+ public void close() {
+ this.ldapStoreRegistry = null;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_NAME;
+ }
+
+ // Best effort to create appropriate mappers according to our LDAP config
+ @Override
+ public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+ LDAPConfig ldapConfig = new LDAPConfig(model.getConfig());
+
+ boolean activeDirectory = ldapConfig.isActiveDirectory();
+ EditMode editMode = ldapConfig.getEditMode();
+ String readOnly = String.valueOf(editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED);
+ String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
+
+ String alwaysReadValueFromLDAP = String.valueOf(editMode==EditMode.READ_ONLY || editMode== EditMode.WRITABLE);
+
+ ComponentModel mapperModel;
+ mapperModel = KeycloakModelUtils.createComponentModel("username", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+
+ // CN is typically used as RDN for Active Directory deployments
+ if (ldapConfig.getRdnLdapAttribute().equalsIgnoreCase(LDAPConstants.CN)) {
+
+ if (usernameLdapAttribute.equalsIgnoreCase(LDAPConstants.CN)) {
+
+ // For AD deployments with "cn" as username, we will map "givenName" to first name
+ mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+
+ } else {
+ if (editMode == EditMode.WRITABLE) {
+
+ // For AD deployments with "sAMAccountName" as username and writable, we need to map "cn" as username as well (this is needed so we can register new users from KC into LDAP) and we will map "givenName" to first name.
+ mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+
+ mapperModel = KeycloakModelUtils.createComponentModel("username-cn", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+ } else {
+
+ // For read-only LDAP, we map "cn" as full name
+ mapperModel = KeycloakModelUtils.createComponentModel("full name", model.getId(), FullNameLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
+ FullNameLDAPStorageMapper.READ_ONLY, readOnly,
+ FullNameLDAPStorageMapper.WRITE_ONLY, "false");
+ realm.addComponentModel(mapperModel);
+ }
+ }
+ } else {
+ mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+ }
+
+ mapperModel = KeycloakModelUtils.createComponentModel("last name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+ realm.addComponentModel(mapperModel);
+
+ mapperModel = KeycloakModelUtils.createComponentModel("email", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
+ UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+ realm.addComponentModel(mapperModel);
+
+ String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
+ String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
+
+ // map createTimeStamp as read-only
+ mapperModel = KeycloakModelUtils.createComponentModel("creation date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
+ UserAttributeLDAPStorageMapper.READ_ONLY, "true",
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+ realm.addComponentModel(mapperModel);
+
+ // map modifyTimeStamp as read-only
+ mapperModel = KeycloakModelUtils.createComponentModel("modify date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+ UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
+ UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
+ UserAttributeLDAPStorageMapper.READ_ONLY, "true",
+ UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+ realm.addComponentModel(mapperModel);
+
+ // MSAD specific mapper for account state propagation
+ if (activeDirectory) {
+ mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName());
+ realm.addComponentModel(mapperModel);
+ }
+ }
+
+ @Override
+ public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+ syncMappers(sessionFactory, realmId, model);
+
+ logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getName());
+
+ LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
+ SynchronizationResult syncResult = syncImpl(sessionFactory, userQuery, realmId, model);
+
+ // TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
+
+ logger.infof("Sync all users finished: %s", syncResult.getStatus());
+ return syncResult;
+ }
+
+ @Override
+ public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+ syncMappers(sessionFactory, realmId, model);
+
+ logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getName());
+
+ // Sync newly created and updated users
+ LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+ Condition createCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.CREATE_TIMESTAMP, lastSync);
+ Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.MODIFY_TIMESTAMP, lastSync);
+ Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
+
+ LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
+ userQuery.addWhereCondition(orCondition);
+ SynchronizationResult result = syncImpl(sessionFactory, userQuery, realmId, model);
+
+ logger.infof("Sync changed users finished: %s", result.getStatus());
+ return result;
+ }
+
+ protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPStorageProvider ldapProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
+ RealmModel realm = session.realms().getRealm(realmId);
+ List mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+ for (ComponentModel mapperModel : mappers) {
+ LDAPStorageMapper ldapMapper = session.getProvider(LDAPStorageMapper.class, mapperModel);
+ SynchronizationResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+ if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) {
+ logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString());
+ }
+ }
+ }
+
+ });
+ }
+
+ protected SynchronizationResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final ComponentModel fedModel) {
+
+ final SynchronizationResult syncResult = new SynchronizationResult();
+
+ LDAPConfig ldapConfig = new LDAPConfig(fedModel.getConfig());
+ boolean pagination = ldapConfig.isPagination();
+ if (pagination) {
+ int pageSize = ldapConfig.getBatchSizeForSync();
+
+ boolean nextPage = true;
+ while (nextPage) {
+ userQuery.setLimit(pageSize);
+ final List users = userQuery.getResultList();
+ nextPage = userQuery.getPaginationContext() != null;
+ SynchronizationResult currentPageSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+ syncResult.add(currentPageSync);
+ }
+ } else {
+ // LDAP pagination not available. Do everything in single transaction
+ final List users = userQuery.getResultList();
+ SynchronizationResult currentSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+ syncResult.add(currentSync);
+ }
+
+ return syncResult;
+ }
+
+ private LDAPQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
+ class QueryHolder {
+ LDAPQuery query;
+ }
+
+ final QueryHolder queryHolder = new QueryHolder();
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
+ RealmModel realm = session.realms().getRealm(realmId);
+ queryHolder.query = LDAPUtils.createQueryForUserSearch(ldapFedProvider, realm);
+ }
+
+ });
+ return queryHolder.query;
+ }
+
+ protected SynchronizationResult importLdapUsers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel fedModel, List ldapUsers) {
+ final SynchronizationResult syncResult = new SynchronizationResult();
+
+ class BooleanHolder {
+ private boolean value = true;
+ }
+ final BooleanHolder exists = new BooleanHolder();
+
+ for (final LDAPObject ldapUser : ldapUsers) {
+
+ try {
+
+ // Process each user in it's own transaction to avoid global fail
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
+ RealmModel currentRealm = session.realms().getRealm(realmId);
+
+ String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+ exists.value = true;
+ LDAPUtils.checkUuid(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+ UserModel currentUser = session.userLocalStorage().getUserByUsername(username, currentRealm);
+
+ if (currentUser == null) {
+
+ // Add new user to Keycloak
+ exists.value = false;
+ ldapFedProvider.importUserFromLDAP(session, currentRealm, ldapUser);
+ syncResult.increaseAdded();
+
+ } else {
+ if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
+
+ // Update keycloak user
+ List federationMappers = currentRealm.getComponents(fedModel.getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = ldapFedProvider.sortMappersDesc(federationMappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ LDAPStorageMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
+ ldapMapper.onImportUserFromLDAP(mapperModel, ldapFedProvider, ldapUser, currentUser, currentRealm, false);
+ }
+
+ logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
+ syncResult.increaseUpdated();
+ } else {
+ logger.warnf("User '%s' is not updated during sync as he already exists in Keycloak database but is not linked to federation provider '%s'", username, fedModel.getName());
+ syncResult.increaseFailed();
+ }
+ }
+ }
+
+ });
+ } catch (ModelException me) {
+ logger.error("Failed during import user from LDAP", me);
+ syncResult.increaseFailed();
+
+ // Remove user if we already added him during this transaction
+ if (!exists.value) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
+ RealmModel currentRealm = session.realms().getRealm(realmId);
+ String username = null;
+ try {
+ username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+ } catch (ModelException ignore) {
+ }
+
+ if (username != null) {
+ UserModel existing = session.userLocalStorage().getUserByUsername(username, currentRealm);
+ if (existing != null) {
+ session.getUserCache().evict(currentRealm, existing);
+ session.userLocalStorage().removeUser(currentRealm, existing);
+ }
+ }
+ }
+
+ });
+ }
+ }
+ }
+
+ return syncResult;
+ }
+
+ protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {
+ KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(kerberosConfig);
+ return new SPNEGOAuthenticator(kerberosConfig, kerberosAuth, spnegoToken);
+ }
+
+ protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator(CommonKerberosConfig kerberosConfig) {
+ return new KerberosServerSubjectAuthenticator(kerberosConfig);
+ }
+
+ protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAuthenticator(CommonKerberosConfig kerberosConfig) {
+ return new KerberosUsernamePasswordAuthenticator(kerberosConfig);
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
new file mode 100755
index 00000000000..08f00a80a7c
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
@@ -0,0 +1,286 @@
+/*
+ * 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.storage.ldap;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.storage.ldap.idm.model.LDAPDn;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.membership.MembershipType;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Allow to directly call some operations against LDAPIdentityStore.
+ *
+ * @author Marek Posolda
+ */
+public class LDAPUtils {
+
+ /**
+ * @param ldapProvider
+ * @param realm
+ * @param user
+ * @return newly created LDAPObject with all the attributes, uuid and DN properly set
+ */
+ public static LDAPObject addUserToLDAP(LDAPStorageProvider ldapProvider, RealmModel realm, UserModel user) {
+ LDAPObject ldapUser = new LDAPObject();
+
+ LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
+ LDAPConfig ldapConfig = ldapStore.getConfig();
+ ldapUser.setRdnAttributeName(ldapConfig.getRdnLdapAttribute());
+ ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
+
+ List federationMappers = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
+ List sortedMappers = ldapProvider.sortMappersAsc(federationMappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ LDAPStorageMapper ldapMapper = ldapProvider.getMapper(mapperModel);
+ ldapMapper.onRegisterUserToLDAP(mapperModel, ldapProvider, ldapUser, user, realm);
+ }
+
+ LDAPUtils.computeAndSetDn(ldapConfig, ldapUser);
+ ldapStore.add(ldapUser);
+ return ldapUser;
+ }
+
+ public static LDAPQuery createQueryForUserSearch(LDAPStorageProvider ldapProvider, RealmModel realm) {
+ LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
+ LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig();
+ ldapQuery.setSearchScope(config.getSearchScope());
+ ldapQuery.setSearchDn(config.getUsersDn());
+ ldapQuery.addObjectClasses(config.getUserObjectClasses());
+
+ String customFilter = config.getCustomUserSearchFilter();
+ if (customFilter != null) {
+ Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
+ ldapQuery.addWhereCondition(customFilterCondition);
+ }
+
+ List mapperModels = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
+ ldapQuery.addMappers(mapperModels);
+
+ return ldapQuery;
+ }
+
+ // ldapUser has filled attributes, but doesn't have filled dn.
+ private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) {
+ String rdnLdapAttrName = config.getRdnLdapAttribute();
+ String rdnLdapAttrValue = ldapUser.getAttributeAsString(rdnLdapAttrName);
+ if (rdnLdapAttrValue == null) {
+ throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes());
+ }
+
+ LDAPDn dn = LDAPDn.fromString(config.getUsersDn());
+ dn.addFirst(rdnLdapAttrName, rdnLdapAttrValue);
+ ldapUser.setDn(dn);
+ }
+
+ public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
+ String usernameAttr = config.getUsernameLdapAttribute();
+ String ldapUsername = ldapUser.getAttributeAsString(usernameAttr);
+
+ if (ldapUsername == null) {
+ throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. Mapped username LDAP attribute: " +
+ config.getUsernameLdapAttribute() + ", user DN: " + ldapUser.getDn() + ", attributes from LDAP: " + ldapUser.getAttributes());
+ }
+
+ return ldapUsername;
+ }
+
+ public static void checkUuid(LDAPObject ldapUser, LDAPConfig config) {
+ if (ldapUser.getUuid() == null) {
+ throw new ModelException("User returned from LDAP has null uuid! Check configuration of your LDAP settings. UUID Attribute must be unique among your LDAP records and available on all the LDAP user records. " +
+ "If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN' . " +
+ "Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn());
+ }
+ }
+
+
+ // roles & groups
+
+ public static LDAPObject createLDAPGroup(LDAPStorageProvider ldapProvider, String groupName, String groupNameAttribute, Collection objectClasses,
+ String parentDn, Map> additionalAttributes) {
+ LDAPObject ldapObject = new LDAPObject();
+
+ ldapObject.setRdnAttributeName(groupNameAttribute);
+ ldapObject.setObjectClasses(objectClasses);
+ ldapObject.setSingleAttribute(groupNameAttribute, groupName);
+
+ LDAPDn roleDn = LDAPDn.fromString(parentDn);
+ roleDn.addFirst(groupNameAttribute, groupName);
+ ldapObject.setDn(roleDn);
+
+ for (Map.Entry> attrEntry : additionalAttributes.entrySet()) {
+ ldapObject.setAttribute(attrEntry.getKey(), attrEntry.getValue());
+ }
+
+ ldapProvider.getLdapIdentityStore().add(ldapObject);
+ return ldapObject;
+ }
+
+ /**
+ * Add ldapChild as member of ldapParent and save ldapParent to LDAP.
+ *
+ * @param ldapProvider
+ * @param membershipType how is 'member' attribute saved (full DN or just uid)
+ * @param memberAttrName usually 'member'
+ * @param ldapParent role or group
+ * @param ldapChild usually user (or child group or child role)
+ * @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
+ */
+ public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
+
+ Set memberships = getExistingMemberships(memberAttrName, ldapParent);
+
+ // Remove membership placeholder if present
+ if (membershipType == MembershipType.DN) {
+ for (String membership : memberships) {
+ if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
+ memberships.remove(membership);
+ break;
+ }
+ }
+ }
+
+ String membership = getMemberValueOfChildObject(ldapChild, membershipType);
+
+ memberships.add(membership);
+ ldapParent.setAttribute(memberAttrName, memberships);
+
+ if (sendLDAPUpdateRequest) {
+ ldapProvider.getLdapIdentityStore().update(ldapParent);
+ }
+ }
+
+ /**
+ * Remove ldapChild as member of ldapParent and save ldapParent to LDAP.
+ *
+ * @param ldapProvider
+ * @param membershipType how is 'member' attribute saved (full DN or just uid)
+ * @param memberAttrName usually 'member'
+ * @param ldapParent role or group
+ * @param ldapChild usually user (or child group or child role)
+ * @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
+ */
+ public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
+ Set memberships = getExistingMemberships(memberAttrName, ldapParent);
+
+ String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
+
+ memberships.remove(userMembership);
+
+ // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
+ if (memberships.size() == 0 && membershipType== MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
+ memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
+ }
+
+ ldapParent.setAttribute(memberAttrName, memberships);
+ ldapProvider.getLdapIdentityStore().update(ldapParent);
+ }
+
+ /**
+ * Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
+ *
+ * @param memberAttrName usually 'member'
+ * @param ldapRole
+ * @return
+ */
+ public static Set getExistingMemberships(String memberAttrName, LDAPObject ldapRole) {
+ Set memberships = ldapRole.getAttributeAsSet(memberAttrName);
+ if (memberships == null) {
+ memberships = new HashSet<>();
+ }
+ return memberships;
+ }
+
+ /**
+ * Get value to be used as attribute 'member' in some parent ldapObject
+ */
+ public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
+ return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
+ }
+
+
+ /**
+ * Load all LDAP objects corresponding to given query. We will load them paginated, so we allow to bypass the limitation of 1000
+ * maximum loaded objects in single query in MSAD
+ *
+ * @param ldapQuery
+ * @param ldapProvider
+ * @return
+ */
+ public static List loadAllLDAPObjects(LDAPQuery ldapQuery, LDAPStorageProvider ldapProvider) {
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+ boolean pagination = ldapConfig.isPagination();
+ if (pagination) {
+ // For now reuse globally configured batch size in LDAP provider page
+ int pageSize = ldapConfig.getBatchSizeForSync();
+
+ List result = new LinkedList<>();
+ boolean nextPage = true;
+
+ while (nextPage) {
+ ldapQuery.setLimit(pageSize);
+ final List currentPageGroups = ldapQuery.getResultList();
+ result.addAll(currentPageGroups);
+ nextPage = ldapQuery.getPaginationContext() != null;
+ }
+
+ return result;
+ } else {
+ // LDAP pagination not available. Do everything in single transaction
+ return ldapQuery.getResultList();
+ }
+ }
+
+
+ /**
+ * Validate configured customFilter matches the requested format
+ *
+ * @param customFilter
+ * @throws FederationConfigValidationException
+ */
+ public static void validateCustomLdapFilter(String customFilter) throws ComponentValidationException {
+ if (customFilter != null) {
+
+ customFilter = customFilter.trim();
+ if (customFilter.isEmpty()) {
+ return;
+ }
+
+ if (!customFilter.startsWith("(") || !customFilter.endsWith(")")) {
+ throw new ComponentValidationException("ldapErrorInvalidCustomFilter");
+ }
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/ReadonlyLDAPUserModelDelegate.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/ReadonlyLDAPUserModelDelegate.java
new file mode 100755
index 00000000000..18ed8e2d694
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/ReadonlyLDAPUserModelDelegate.java
@@ -0,0 +1,57 @@
+/*
+ * 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.storage.ldap;
+
+import org.keycloak.models.ModelReadOnlyException;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class ReadonlyLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+
+ protected LDAPStorageProvider provider;
+
+ public ReadonlyLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider) {
+ super(delegate);
+ this.provider = provider;
+ }
+
+ @Override
+ public void setUsername(String username) {
+ throw new ModelReadOnlyException("Federated storage is not writable");
+ }
+
+ @Override
+ public void setLastName(String lastName) {
+ throw new ModelReadOnlyException("Federated storage is not writable");
+ }
+
+ @Override
+ public void setFirstName(String first) {
+ throw new ModelReadOnlyException("Federated storage is not writable");
+ }
+
+ @Override
+ public void setEmail(String email) {
+ throw new ModelReadOnlyException("Federated storage is not writable");
+ }
+
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/UnsyncedLDAPUserModelDelegate.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/UnsyncedLDAPUserModelDelegate.java
new file mode 100755
index 00000000000..e26104c8238
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/UnsyncedLDAPUserModelDelegate.java
@@ -0,0 +1,37 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class UnsyncedLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+ private static final Logger logger = Logger.getLogger(UnsyncedLDAPUserModelDelegate.class);
+
+ protected LDAPStorageProvider provider;
+
+ public UnsyncedLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider) {
+ super(delegate);
+ this.provider = provider;
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/WritableLDAPUserModelDelegate.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/WritableLDAPUserModelDelegate.java
new file mode 100755
index 00000000000..6b87bb80c36
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/WritableLDAPUserModelDelegate.java
@@ -0,0 +1,41 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class WritableLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+ private static final Logger logger = Logger.getLogger(WritableLDAPUserModelDelegate.class);
+
+ protected LDAPStorageProvider provider;
+ protected LDAPObject ldapObject;
+
+ public WritableLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider, LDAPObject ldapObject) {
+ super(delegate);
+ this.provider = provider;
+ this.ldapObject = ldapObject;
+ }
+
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java
new file mode 100644
index 00000000000..e95e8adafd8
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java
@@ -0,0 +1,153 @@
+/*
+ * 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.storage.ldap.idm.model;
+
+import javax.naming.ldap.Rdn;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.LinkedList;
+
+/**
+ * @author Marek Posolda
+ */
+public class LDAPDn {
+
+ private final Deque entries = new LinkedList<>();
+
+ public static LDAPDn fromString(String dnString) {
+ LDAPDn dn = new LDAPDn();
+
+ // In certain OpenLDAP implementations the uniqueMember attribute is mandatory
+ // Thus, if a new group is created, it will contain an empty uniqueMember attribute
+ // Later on, when adding members, this empty attribute will be kept
+ // Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
+ if(dnString.trim().isEmpty())
+ return dn;
+
+ String[] rdns = dnString.split("(? entries) {
+ StringBuilder builder = new StringBuilder();
+
+ boolean first = true;
+ for (Entry rdn : entries) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(",");
+ }
+ builder.append(rdn.attrName).append("=").append(rdn.attrValue);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * @return string like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
+ */
+ public String getFirstRdn() {
+ Entry firstEntry = entries.getFirst();
+ return firstEntry.attrName + "=" + firstEntry.attrValue;
+ }
+
+ /**
+ * @return string attribute name like "uid" from the DN like "uid=joe,dc=something,dc=org"
+ */
+ public String getFirstRdnAttrName() {
+ Entry firstEntry = entries.getFirst();
+ return firstEntry.attrName;
+ }
+
+ /**
+ * @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org"
+ */
+ public String getFirstRdnAttrValue() {
+ Entry firstEntry = entries.getFirst();
+ return firstEntry.attrValue;
+ }
+
+ /**
+ *
+ * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
+ */
+ public String getParentDn() {
+ LinkedList parentDnEntries = new LinkedList<>(entries);
+ parentDnEntries.remove();
+ return toString(parentDnEntries);
+ }
+
+ public boolean isDescendantOf(LDAPDn expectedParentDn) {
+ int parentEntriesCount = expectedParentDn.entries.size();
+
+ Deque myEntries = new LinkedList<>(this.entries);
+ boolean someRemoved = false;
+ while (myEntries.size() > parentEntriesCount) {
+ myEntries.removeFirst();
+ someRemoved = true;
+ }
+
+ String myEntriesParentStr = toString(myEntries).toLowerCase();
+ String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
+ return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
+ }
+
+ public void addFirst(String rdnName, String rdnValue) {
+ rdnValue = Rdn.escapeValue(rdnValue);
+ entries.addFirst(new Entry(rdnName, rdnValue));
+ }
+
+ private void addLast(String rdnName, String rdnValue) {
+ entries.addLast(new Entry(rdnName, rdnValue));
+ }
+
+ private static class Entry {
+ private final String attrName;
+ private final String attrValue;
+
+ private Entry(String attrName, String attrValue) {
+ this.attrName = attrName;
+ this.attrValue = attrValue;
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java
new file mode 100644
index 00000000000..64ef65fd072
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java
@@ -0,0 +1,157 @@
+/*
+ * 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.storage.ldap.idm.model;
+
+import org.jboss.logging.Logger;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Marek Posolda
+ */
+public class LDAPObject {
+
+ private static final Logger logger = Logger.getLogger(LDAPObject.class);
+
+ private String uuid;
+ private LDAPDn dn;
+ private String rdnAttributeName;
+
+ private final List objectClasses = new LinkedList<>();
+
+ // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
+ private final List readOnlyAttributeNames = new LinkedList<>();
+
+ private final Map> attributes = new HashMap<>();
+
+ // Copy of "attributes" containing lower-cased keys
+ private final Map> lowerCasedAttributes = new HashMap<>();
+
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public LDAPDn getDn() {
+ return dn;
+ }
+
+ public void setDn(LDAPDn dn) {
+ this.dn = dn;
+ }
+
+ public List getObjectClasses() {
+ return objectClasses;
+ }
+
+ public void setObjectClasses(Collection objectClasses) {
+ this.objectClasses.clear();
+ this.objectClasses.addAll(objectClasses);
+ }
+
+ public List getReadOnlyAttributeNames() {
+ return readOnlyAttributeNames;
+ }
+
+ public void addReadOnlyAttributeName(String readOnlyAttribute) {
+ readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
+ }
+
+ public void removeReadOnlyAttributeName(String readOnlyAttribute) {
+ readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
+ }
+
+ public String getRdnAttributeName() {
+ return rdnAttributeName;
+ }
+
+ public void setRdnAttributeName(String rdnAttributeName) {
+ this.rdnAttributeName = rdnAttributeName;
+ }
+
+ public void setSingleAttribute(String attributeName, String attributeValue) {
+ Set asSet = new LinkedHashSet<>();
+ asSet.add(attributeValue);
+ setAttribute(attributeName, asSet);
+ }
+
+ public void setAttribute(String attributeName, Set attributeValue) {
+ attributes.put(attributeName, attributeValue);
+ lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
+ }
+
+ // Case-insensitive
+ public String getAttributeAsString(String name) {
+ Set attrValue = lowerCasedAttributes.get(name.toLowerCase());
+ if (attrValue == null || attrValue.size() == 0) {
+ return null;
+ } else if (attrValue.size() > 1) {
+ logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
+ }
+
+ return attrValue.iterator().next();
+ }
+
+ // Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
+ public Set getAttributeAsSet(String name) {
+ Set values = lowerCasedAttributes.get(name.toLowerCase());
+ return (values == null) ? null : new LinkedHashSet<>(values);
+ }
+
+
+ public Map> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (!getClass().isInstance(obj)) {
+ return false;
+ }
+
+ LDAPObject other = (LDAPObject) obj;
+
+ return getUuid() != null && other.getUuid() != null && getUuid().equals(other.getUuid());
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getUuid() != null ? getUuid().hashCode() : 0;
+ result = 31 * result + (getUuid() != null ? getUuid().hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "LDAP Object [ dn: " + dn + " , uuid: " + uuid + ", attributes: " + attributes + ", readOnly attribute names: " + readOnlyAttributeNames + " ]";
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Condition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Condition.java
new file mode 100644
index 00000000000..152b0889ad0
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Condition.java
@@ -0,0 +1,42 @@
+/*
+ * 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.storage.ldap.idm.query;
+
+/**
+ *
A {@link Condition} is used to specify how a specific query parameter
+ * is defined in order to filter query results.
+ *
+ * @author Pedro Igor
+ */
+public interface Condition {
+
+ String getParameterName();
+ void setParameterName(String parameterName);
+
+ /**
+ * Will change the parameter name if it is "modelParamName" to "ldapParamName" . Implementation can apply this to subconditions as well.
+ *
+ * It is used to update LDAP queries, which were created with model parameter name ( for example "firstName" ) and rewrite them to use real
+ * LDAP mapped attribute (for example "givenName" )
+ */
+ void updateParameterName(String modelParamName, String ldapParamName);
+
+
+ void applyCondition(StringBuilder filter);
+
+}
\ No newline at end of file
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Sort.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Sort.java
new file mode 100644
index 00000000000..97e381d0523
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/Sort.java
@@ -0,0 +1,40 @@
+/*
+ * 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.storage.ldap.idm.query;
+
+/**
+ * @author Pedro Igor
+ */
+public class Sort {
+
+ private final String paramName;
+ private final boolean asc;
+
+ public Sort(String paramName, boolean asc) {
+ this.paramName = paramName;
+ this.asc = asc;
+ }
+
+ public String getParameter() {
+ return this.paramName;
+ }
+
+ public boolean isAscending() {
+ return asc;
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/BetweenCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/BetweenCondition.java
new file mode 100644
index 00000000000..dedc29d71b2
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/BetweenCondition.java
@@ -0,0 +1,53 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
+
+import java.util.Date;
+
+/**
+ * @author Pedro Igor
+ */
+class BetweenCondition extends NamedParameterCondition {
+
+ private final Comparable x;
+ private final Comparable y;
+
+ public BetweenCondition(String name, Comparable x, Comparable y) {
+ super(name);
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ Comparable x = this.x;
+ Comparable y = this.y;
+
+ if (Date.class.isInstance(x)) {
+ x = LDAPUtil.formatDate((Date) x);
+ }
+
+ if (Date.class.isInstance(y)) {
+ y = LDAPUtil.formatDate((Date) y);
+ }
+
+ filter.append("(").append(x).append("<=").append(getParameterName()).append("<=").append(y).append(")");
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/CustomLDAPFilter.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/CustomLDAPFilter.java
new file mode 100644
index 00000000000..c65a4754cb1
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/CustomLDAPFilter.java
@@ -0,0 +1,51 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.query.Condition;
+
+/**
+ * @author Marek Posolda
+ */
+class CustomLDAPFilter implements Condition {
+
+ private final String customFilter;
+
+ public CustomLDAPFilter(String customFilter) {
+ this.customFilter = customFilter;
+ }
+
+ @Override
+ public String getParameterName() {
+ return null;
+ }
+
+ @Override
+ public void setParameterName(String parameterName) {
+ }
+
+ @Override
+ public void updateParameterName(String modelParamName, String ldapParamName) {
+
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ filter.append(customFilter);
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/EqualCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/EqualCondition.java
new file mode 100644
index 00000000000..e82fe376d23
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/EqualCondition.java
@@ -0,0 +1,58 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
+
+import java.util.Date;
+
+/**
+ * @author Pedro Igor
+ */
+public class EqualCondition extends NamedParameterCondition {
+
+ private final Object value;
+
+ public EqualCondition(String name, Object value) {
+ super(name);
+ this.value = value;
+ }
+
+ public Object getValue() {
+ return this.value;
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ Object parameterValue = value;
+ if (Date.class.isInstance(value)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(parameterValue).append(")");
+ }
+
+ @Override
+ public String toString() {
+ return "EqualCondition{" +
+ "paramName=" + getParameterName() +
+ ", value=" + value +
+ '}';
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/GreaterThanCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/GreaterThanCondition.java
new file mode 100644
index 00000000000..32432e63ba4
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/GreaterThanCondition.java
@@ -0,0 +1,53 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
+
+import java.util.Date;
+
+/**
+ * @author Pedro Igor
+ */
+class GreaterThanCondition extends NamedParameterCondition {
+
+ private final boolean orEqual;
+
+ private final Comparable value;
+
+ public GreaterThanCondition(String name, Comparable value, boolean orEqual) {
+ super(name);
+ this.value = value;
+ this.orEqual = orEqual;
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ Comparable parameterValue = value;
+
+ if (Date.class.isInstance(parameterValue)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ if (orEqual) {
+ filter.append("(").append(getParameterName()).append(">=").append(parameterValue).append(")");
+ } else {
+ filter.append("(").append(getParameterName()).append(">").append(parameterValue).append(")");
+ }
+ }
+}
\ No newline at end of file
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/InCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/InCondition.java
new file mode 100644
index 00000000000..8f5c26a0e55
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/InCondition.java
@@ -0,0 +1,48 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.models.LDAPConstants;
+
+/**
+ * @author Pedro Igor
+ */
+class InCondition extends NamedParameterCondition {
+
+ private final Object[] valuesToCompare;
+
+ public InCondition(String name, Object[] valuesToCompare) {
+ super(name);
+ this.valuesToCompare = valuesToCompare;
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+
+ filter.append("(&(");
+
+ for (int i = 0; i< valuesToCompare.length; i++) {
+ Object value = valuesToCompare[i];
+
+ filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(value).append(")");
+ }
+
+ filter.append("))");
+ }
+}
+
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java
new file mode 100644
index 00000000000..eb7ff1bb9ac
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java
@@ -0,0 +1,213 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.Sort;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+
+import javax.naming.directory.SearchControls;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.Collections.unmodifiableSet;
+
+/**
+ * Default IdentityQuery implementation.
+ *
+ *
+ * @author Shane Bryzak
+ */
+public class LDAPQuery {
+
+ private final LDAPStorageProvider ldapFedProvider;
+
+ private int offset;
+ private int limit;
+ private byte[] paginationContext;
+ private String searchDn;
+ private final Set conditions = new LinkedHashSet();
+ private final Set ordering = new LinkedHashSet();
+
+ private final Set returningLdapAttributes = new LinkedHashSet();
+
+ // Contains just those returningLdapAttributes, which are read-only. They will be marked as read-only in returned LDAPObject instances as well
+ // NOTE: names of attributes are lower-cased to avoid case sensitivity issues (LDAP searching is usually case-insensitive, so we want to be as well)
+ private final Set returningReadOnlyLdapAttributes = new LinkedHashSet();
+ private final Set objectClasses = new LinkedHashSet();
+
+ private final List mappers = new ArrayList<>();
+
+ private int searchScope = SearchControls.SUBTREE_SCOPE;
+
+ public LDAPQuery(LDAPStorageProvider ldapProvider) {
+ this.ldapFedProvider = ldapProvider;
+ }
+
+ public LDAPQuery addWhereCondition(Condition... condition) {
+ this.conditions.addAll(Arrays.asList(condition));
+ return this;
+ }
+
+ public LDAPQuery sortBy(Sort... sorts) {
+ this.ordering.addAll(Arrays.asList(sorts));
+ return this;
+ }
+
+ public LDAPQuery setSearchDn(String searchDn) {
+ this.searchDn = searchDn;
+ return this;
+ }
+
+ public LDAPQuery addObjectClasses(Collection objectClasses) {
+ this.objectClasses.addAll(objectClasses);
+ return this;
+ }
+
+ public LDAPQuery addReturningLdapAttribute(String ldapAttributeName) {
+ this.returningLdapAttributes.add(ldapAttributeName);
+ return this;
+ }
+
+ public LDAPQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
+ this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase());
+ return this;
+ }
+
+ public LDAPQuery addMappers(Collection mappers) {
+ this.mappers.addAll(mappers);
+ return this;
+ }
+
+ public LDAPQuery setSearchScope(int searchScope) {
+ this.searchScope = searchScope;
+ return this;
+ }
+
+ public Set getSorting() {
+ return unmodifiableSet(this.ordering);
+ }
+
+ public String getSearchDn() {
+ return this.searchDn;
+ }
+
+ public Set getObjectClasses() {
+ return unmodifiableSet(this.objectClasses);
+ }
+
+ public Set getReturningLdapAttributes() {
+ return unmodifiableSet(this.returningLdapAttributes);
+ }
+
+ public Set getReturningReadOnlyLdapAttributes() {
+ return unmodifiableSet(this.returningReadOnlyLdapAttributes);
+ }
+
+ public List getMappers() {
+ return mappers;
+ }
+
+ public int getSearchScope() {
+ return searchScope;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public byte[] getPaginationContext() {
+ return paginationContext;
+ }
+
+
+ public List getResultList() {
+
+ // Apply mappers now
+ List sortedMappers = ldapFedProvider.sortMappersAsc(mappers);
+ for (ComponentModel mapperModel : sortedMappers) {
+ LDAPStorageMapper fedMapper = ldapFedProvider.getMapper(mapperModel);
+ fedMapper.beforeLDAPQuery(mapperModel, this);
+ }
+
+ List result = new ArrayList();
+
+ try {
+ for (LDAPObject ldapObject : ldapFedProvider.getLdapIdentityStore().fetchQueryResults(this)) {
+ result.add(ldapObject);
+ }
+ } catch (Exception e) {
+ throw new ModelException("LDAP Query failed", e);
+ }
+
+ return result;
+ }
+
+ public LDAPObject getFirstResult() {
+ List results = getResultList();
+
+ if (results.isEmpty()) {
+ return null;
+ } else if (results.size() == 1) {
+ return results.get(0);
+ } else {
+ throw new ModelDuplicateException("Error - multiple LDAP objects found but expected just one");
+ }
+ }
+
+ public int getResultCount() {
+ return ldapFedProvider.getLdapIdentityStore().countQueryResults(this);
+ }
+
+ public LDAPQuery setOffset(int offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ public LDAPQuery setLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ public LDAPQuery setPaginationContext(byte[] paginationContext) {
+ this.paginationContext = paginationContext;
+ return this;
+ }
+
+ public Set getConditions() {
+ return this.conditions;
+ }
+
+ public LDAPStorageProvider getLdapProvider() {
+ return ldapFedProvider;
+ }
+
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java
new file mode 100644
index 00000000000..715ec3da367
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java
@@ -0,0 +1,84 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.Sort;
+
+/**
+ * @author Pedro Igor
+ */
+public class LDAPQueryConditionsBuilder {
+
+ public Condition equal(String parameter, Object value) {
+ return new EqualCondition(parameter, value);
+ }
+
+ public Condition greaterThan(String paramName, Object x) {
+ throwExceptionIfNotComparable(x);
+ return new GreaterThanCondition(paramName, (Comparable) x, false);
+ }
+
+ public Condition greaterThanOrEqualTo(String paramName, Object x) {
+ throwExceptionIfNotComparable(x);
+ return new GreaterThanCondition(paramName, (Comparable) x, true);
+ }
+
+ public Condition lessThan(String paramName, Comparable x) {
+ return new LessThanCondition(paramName, x, false);
+ }
+
+ public Condition lessThanOrEqualTo(String paramName, Comparable x) {
+ return new LessThanCondition(paramName, x, true);
+ }
+
+ public Condition between(String paramName, Comparable x, Comparable y) {
+ return new BetweenCondition(paramName, x, y);
+ }
+
+ public Condition orCondition(Condition... conditions) {
+ if (conditions == null || conditions.length == 0) {
+ throw new ModelException("At least one condition should be provided to OR query");
+ }
+ return new OrCondition(conditions);
+ }
+
+ public Condition addCustomLDAPFilter(String filter) {
+ filter = filter.trim();
+ return new CustomLDAPFilter(filter);
+ }
+
+ public Condition in(String paramName, Object... x) {
+ return new InCondition(paramName, x);
+ }
+
+ public Sort asc(String paramName) {
+ return new Sort(paramName, true);
+ }
+
+ public Sort desc(String paramName) {
+ return new Sort(paramName, false);
+ }
+
+ private void throwExceptionIfNotComparable(Object x) {
+ if (!Comparable.class.isInstance(x)) {
+ throw new ModelException("Query parameter value [" + x + "] must be " + Comparable.class + ".");
+ }
+ }
+}
\ No newline at end of file
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LessThanCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LessThanCondition.java
new file mode 100644
index 00000000000..a32fb27867e
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LessThanCondition.java
@@ -0,0 +1,53 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
+
+import java.util.Date;
+
+/**
+ * @author Pedro Igor
+ */
+class LessThanCondition extends NamedParameterCondition {
+
+ private final boolean orEqual;
+
+ private final Comparable value;
+
+ public LessThanCondition(String name, Comparable value, boolean orEqual) {
+ super(name);
+ this.value = value;
+ this.orEqual = orEqual;
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ Comparable parameterValue = value;
+
+ if (Date.class.isInstance(parameterValue)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ if (orEqual) {
+ filter.append("(").append(getParameterName()).append("<=").append(parameterValue).append(")");
+ } else {
+ filter.append("(").append(getParameterName()).append("<").append(parameterValue).append(")");
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/NamedParameterCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/NamedParameterCondition.java
new file mode 100644
index 00000000000..72a9a0cb817
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/NamedParameterCondition.java
@@ -0,0 +1,50 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.query.Condition;
+
+/**
+ * @author Marek Posolda
+ */
+public abstract class NamedParameterCondition implements Condition {
+
+ private String parameterName;
+
+ public NamedParameterCondition(String parameterName) {
+ this.parameterName = parameterName;
+ }
+
+ @Override
+ public String getParameterName() {
+ return parameterName;
+ }
+
+ @Override
+ public void setParameterName(String parameterName) {
+ this.parameterName = parameterName;
+ }
+
+
+ @Override
+ public void updateParameterName(String modelParamName, String ldapParamName) {
+ if (parameterName.equalsIgnoreCase(modelParamName)) {
+ this.parameterName = ldapParamName;
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/OrCondition.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/OrCondition.java
new file mode 100644
index 00000000000..f605f9a97a6
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/query/internal/OrCondition.java
@@ -0,0 +1,59 @@
+/*
+ * 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.storage.ldap.idm.query.internal;
+
+import org.keycloak.storage.ldap.idm.query.Condition;
+
+/**
+ * @author Marek Posolda
+ */
+class OrCondition implements Condition {
+
+ private final Condition[] innerConditions;
+
+ public OrCondition(Condition... innerConditions) {
+ this.innerConditions = innerConditions;
+ }
+
+ @Override
+ public String getParameterName() {
+ return null;
+ }
+
+ @Override
+ public void setParameterName(String parameterName) {
+ }
+
+ @Override
+ public void updateParameterName(String modelParamName, String ldapParamName) {
+ for (Condition innerCondition : innerConditions) {
+ innerCondition.updateParameterName(modelParamName, ldapParamName);
+ }
+ }
+
+ @Override
+ public void applyCondition(StringBuilder filter) {
+ filter.append("(|");
+
+ for (Condition innerCondition : innerConditions) {
+ innerCondition.applyCondition(filter);
+ }
+
+ filter.append(")");
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java
new file mode 100644
index 00000000000..4b2010b0728
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java
@@ -0,0 +1,98 @@
+/*
+ * 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.storage.ldap.idm.store;
+
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+
+import javax.naming.AuthenticationException;
+import java.util.List;
+
+/**
+ * IdentityStore representation providing minimal SPI
+ *
+ * TODO: Rather remove this abstraction
+ *
+ * @author Boleslaw Dawidowicz
+ * @author Shane Bryzak
+ */
+public interface IdentityStore {
+
+ /**
+ * Returns the configuration for this IdentityStore instance
+ *
+ * @return
+ */
+ LDAPConfig getConfig();
+
+ // General
+
+ /**
+ * Persists the specified IdentityType
+ *
+ * @param ldapObject
+ */
+ void add(LDAPObject ldapObject);
+
+ /**
+ * Updates the specified IdentityType
+ *
+ * @param ldapObject
+ */
+ void update(LDAPObject ldapObject);
+
+ /**
+ * Removes the specified IdentityType
+ *
+ * @param ldapObject
+ */
+ void remove(LDAPObject ldapObject);
+
+ // Identity query
+
+ List fetchQueryResults(LDAPQuery LDAPQuery);
+
+ int countQueryResults(LDAPQuery LDAPQuery);
+
+// // Relationship query
+//
+// List fetchQueryResults(RelationshipQuery query);
+//
+// int countQueryResults(RelationshipQuery query);
+
+ // Credentials
+
+ /**
+ * Validates the specified credentials.
+ *
+ * @param user Keycloak user
+ * @param password Ldap password
+ * @throws AuthenticationException if authentication is not successful
+ */
+ void validatePassword(LDAPObject user, String password) throws AuthenticationException;
+
+ /**
+ * Updates the specified credential value.
+ *
+ * @param user Keycloak user
+ * @param password Ldap password
+ */
+ void updatePassword(LDAPObject user, String password);
+
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
new file mode 100644
index 00000000000..6d0e2cc0b33
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -0,0 +1,423 @@
+/*
+ * 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.storage.ldap.idm.store.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.idm.model.LDAPDn;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.idm.store.IdentityStore;
+
+import javax.naming.AuthenticationException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * An IdentityStore implementation backed by an LDAP directory
+ *
+ * @author Shane Bryzak
+ * @author Anil Saldhana
+ * @author Pedro Silva
+ */
+public class LDAPIdentityStore implements IdentityStore {
+
+ private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
+
+ private final LDAPConfig config;
+ private final LDAPOperationManager operationManager;
+
+ public LDAPIdentityStore(LDAPConfig config) {
+ this.config = config;
+
+ try {
+ this.operationManager = new LDAPOperationManager(config);
+ } catch (NamingException e) {
+ throw new ModelException("Couldn't init operation manager", e);
+ }
+ }
+
+ @Override
+ public LDAPConfig getConfig() {
+ return this.config;
+ }
+
+ @Override
+ public void add(LDAPObject ldapObject) {
+ // id will be assigned by the ldap server
+ if (ldapObject.getUuid() != null) {
+ throw new ModelException("Can't add object with already assigned uuid");
+ }
+
+ String entryDN = ldapObject.getDn().toString();
+ BasicAttributes ldapAttributes = extractAttributes(ldapObject, true);
+ this.operationManager.createSubContext(entryDN, ldapAttributes);
+ ldapObject.setUuid(getEntryIdentifier(ldapObject));
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), entryDN);
+ }
+ }
+
+ @Override
+ public void update(LDAPObject ldapObject) {
+ BasicAttributes updatedAttributes = extractAttributes(ldapObject, false);
+ NamingEnumeration attributes = updatedAttributes.getAll();
+
+ String entryDn = ldapObject.getDn().toString();
+ this.operationManager.modifyAttributes(entryDn, attributes);
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), entryDn);
+ }
+ }
+
+ @Override
+ public void remove(LDAPObject ldapObject) {
+ this.operationManager.removeEntry(ldapObject.getDn().toString());
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getUuid(), ldapObject.getDn().toString());
+ }
+ }
+
+
+ @Override
+ public List fetchQueryResults(LDAPQuery identityQuery) {
+ if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
+ throw new ModelException("LDAP Identity Store does not yet support sorted queries.");
+ }
+
+ List results = new ArrayList<>();
+
+ try {
+ String baseDN = identityQuery.getSearchDn();
+
+ for (Condition condition : identityQuery.getConditions()) {
+
+ // Check if we are searching by ID
+ String uuidAttrName = getConfig().getUuidLDAPAttributeName();
+ if (condition instanceof EqualCondition) {
+ EqualCondition equalCondition = (EqualCondition) condition;
+ if (equalCondition.getParameterName().equalsIgnoreCase(uuidAttrName)) {
+ SearchResult search = this.operationManager
+ .lookupById(baseDN, equalCondition.getValue().toString(), identityQuery.getReturningLdapAttributes());
+
+ if (search != null) {
+ results.add(populateAttributedType(search, identityQuery));
+ }
+
+ return results;
+ }
+ }
+ }
+
+
+ StringBuilder filter = createIdentityTypeSearchFilter(identityQuery);
+
+ List search;
+ if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
+ search = this.operationManager.searchPaginated(baseDN, filter.toString(), identityQuery);
+ } else {
+ search = this.operationManager.search(baseDN, filter.toString(), identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
+ }
+
+ for (SearchResult result : search) {
+ if (!result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
+ results.add(populateAttributedType(result, identityQuery));
+ }
+ }
+ } catch (Exception e) {
+ throw new ModelException("Querying of LDAP failed " + identityQuery, e);
+ }
+
+ return results;
+ }
+
+ @Override
+ public int countQueryResults(LDAPQuery identityQuery) {
+ int limit = identityQuery.getLimit();
+ int offset = identityQuery.getOffset();
+
+ identityQuery.setLimit(0);
+ identityQuery.setOffset(0);
+
+ int resultCount = identityQuery.getResultList().size();
+
+ identityQuery.setLimit(limit);
+ identityQuery.setOffset(offset);
+
+ return resultCount;
+ }
+
+ // *************** CREDENTIALS AND USER SPECIFIC STUFF
+
+ @Override
+ public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
+ String userDN = user.getDn().toString();
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Using DN [%s] for authentication of user", userDN);
+ }
+
+ operationManager.authenticate(userDN, password);
+ }
+
+ @Override
+ public void updatePassword(LDAPObject user, String password) {
+ String userDN = user.getDn().toString();
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Using DN [%s] for updating LDAP password of user", userDN);
+ }
+
+ if (getConfig().isActiveDirectory()) {
+ updateADPassword(userDN, password);
+ } else {
+ ModificationItem[] mods = new ModificationItem[1];
+
+ try {
+ BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
+
+ mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
+
+ operationManager.modifyAttribute(userDN, mod0);
+ } catch (ModelException me) {
+ throw me;
+ } catch (Exception e) {
+ throw new ModelException("Error updating password.", e);
+ }
+ }
+ }
+
+
+ private void updateADPassword(String userDN, String password) {
+ try {
+ // Replace the "unicdodePwd" attribute with a new value
+ // Password must be both Unicode and a quoted string
+ String newQuotedPassword = "\"" + password + "\"";
+ byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
+
+ BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);
+
+ List modItems = new ArrayList();
+ modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
+
+ operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
+ } catch (ModelException me) {
+ throw me;
+ } catch (Exception e) {
+ throw new ModelException(e);
+ }
+ }
+
+ // ************ END CREDENTIALS AND USER SPECIFIC STUFF
+
+ protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
+ StringBuilder filter = new StringBuilder();
+
+ for (Condition condition : identityQuery.getConditions()) {
+ condition.applyCondition(filter);
+ }
+
+ filter.insert(0, "(&");
+ filter.append(getObjectClassesFilter(identityQuery.getObjectClasses()));
+ filter.append(")");
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Using filter for LDAP search: %s . Searching in DN: %s", filter, identityQuery.getSearchDn());
+ }
+ return filter;
+ }
+
+
+ private StringBuilder getObjectClassesFilter(Collection objectClasses) {
+ StringBuilder builder = new StringBuilder();
+
+ if (!objectClasses.isEmpty()) {
+ for (String objectClass : objectClasses) {
+ builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
+ }
+ } else {
+ builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
+ }
+
+ return builder;
+ }
+
+
+ private LDAPObject populateAttributedType(SearchResult searchResult, LDAPQuery ldapQuery) {
+ Set readOnlyAttrNames = ldapQuery.getReturningReadOnlyLdapAttributes();
+ Set lowerCasedAttrNames = new TreeSet<>();
+ for (String attrName : ldapQuery.getReturningLdapAttributes()) {
+ lowerCasedAttrNames.add(attrName.toLowerCase());
+ }
+
+ try {
+ String entryDN = searchResult.getNameInNamespace();
+ Attributes attributes = searchResult.getAttributes();
+
+ LDAPObject ldapObject = new LDAPObject();
+ LDAPDn dn = LDAPDn.fromString(entryDN);
+ ldapObject.setDn(dn);
+ ldapObject.setRdnAttributeName(dn.getFirstRdnAttrName());
+
+ NamingEnumeration extends Attribute> ldapAttributes = attributes.getAll();
+
+ while (ldapAttributes.hasMore()) {
+ Attribute ldapAttribute = ldapAttributes.next();
+
+ try {
+ ldapAttribute.get();
+ } catch (NoSuchElementException nsee) {
+ continue;
+ }
+
+ String ldapAttributeName = ldapAttribute.getID();
+
+ if (ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName())) {
+ Object uuidValue = ldapAttribute.get();
+ ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
+ }
+
+ // Note: UUID is normally not populated here. It's populated just in case that it's used for name of other attribute as well
+ if (!ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName()) || (lowerCasedAttrNames.contains(ldapAttributeName.toLowerCase()))) {
+ Set attrValues = new LinkedHashSet<>();
+ NamingEnumeration> enumm = ldapAttribute.getAll();
+ while (enumm.hasMoreElements()) {
+ String attrVal = enumm.next().toString().trim();
+ attrValues.add(attrVal);
+ }
+
+ if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
+ ldapObject.setObjectClasses(attrValues);
+ } else {
+ ldapObject.setAttribute(ldapAttributeName, attrValues);
+
+ // readOnlyAttrNames are lower-cased
+ if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
+ ldapObject.addReadOnlyAttributeName(ldapAttributeName);
+ }
+ }
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Found ldap object and populated with the attributes. LDAP Object: %s", ldapObject.toString());
+ }
+ return ldapObject;
+
+ } catch (Exception e) {
+ throw new ModelException("Could not populate attribute type " + searchResult.getNameInNamespace() + ".", e);
+ }
+ }
+
+
+ protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
+ BasicAttributes entryAttributes = new BasicAttributes();
+
+ for (Map.Entry> attrEntry : ldapObject.getAttributes().entrySet()) {
+ String attrName = attrEntry.getKey();
+ Set attrValue = attrEntry.getValue();
+
+ // ldapObject.getReadOnlyAttributeNames() are lower-cased
+ if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
+
+ if (attrValue == null) {
+ // Shouldn't happen
+ logger.warnf("Attribute '%s' is null on LDAP object '%s' . Using empty value to be saved to LDAP", attrName, ldapObject.getDn().toString());
+ attrValue = Collections.emptySet();
+ }
+
+ // Ignore empty attributes during create
+ if (isCreate && attrValue.isEmpty()) {
+ continue;
+ }
+
+ BasicAttribute attr = new BasicAttribute(attrName);
+ for (String val : attrValue) {
+ if (val == null || val.toString().trim().length() == 0) {
+ val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
+ }
+ attr.add(val);
+ }
+
+ entryAttributes.put(attr);
+ }
+ }
+
+ // Don't extract object classes for update
+ if (isCreate) {
+ BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);
+
+ for (String objectClassValue : ldapObject.getObjectClasses()) {
+ objectClassAttribute.add(objectClassValue);
+
+ if (objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_NAMES)
+ || objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_ENTRIES)
+ || objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_UNIQUE_NAMES)) {
+ entryAttributes.put(LDAPConstants.MEMBER, LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
+ }
+ }
+
+ entryAttributes.put(objectClassAttribute);
+ }
+
+ return entryAttributes;
+ }
+
+
+ protected String getEntryIdentifier(final LDAPObject ldapObject) {
+ try {
+ // we need this to retrieve the entry's identifier from the ldap server
+ String uuidAttrName = getConfig().getUuidLDAPAttributeName();
+ List search = this.operationManager.search(ldapObject.getDn().toString(), "(" + ldapObject.getDn().getFirstRdn() + ")", Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
+ Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());
+
+ if (id == null) {
+ throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
+ }
+
+ return this.operationManager.decodeEntryUUID(id.get());
+ } catch (NamingException ne) {
+ throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
+ }
+ }
+}
diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
new file mode 100644
index 00000000000..4fe40020e35
--- /dev/null
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -0,0 +1,562 @@
+/*
+ * 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.storage.ldap.idm.store.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+
+import javax.naming.AuthenticationException;
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.Control;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.PagedResultsControl;
+import javax.naming.ldap.PagedResultsResponseControl;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ *
This class provides a set of operations to manage LDAP trees.
+ * Modifies the given {@link javax.naming.directory.Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE
+ * operation.
+ *