From 0bfcbdb359eaad43e7d207884855d20d1855de12 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 6 Jun 2014 09:45:47 +0200 Subject: [PATCH] Support for Active Directory and RHDS --- .../PicketlinkAuthenticationProvider.java | 4 + .../base/resources/js/controllers/realm.js | 6 ++ .../base/resources/partials/realm-ldap.html | 11 +++ .../model/test/LDAPEmbeddedServer.java | 8 ++ .../model/test/AuthProvidersLDAPTest.java | 22 ++--- .../org/keycloak/model/test/ImportTest.java | 3 +- .../tests/src/test/resources/ldap/users.ldif | 4 +- model/tests/src/test/resources/testrealm.json | 3 +- .../AbstractIdentityManagerProvider.java | 32 +++++++ .../idm/LDAPAgentIgnoreCredentialHandler.java | 27 ------ .../idm/LDAPKeycloakCredentialHandler.java | 93 +++++++++++++++++++ .../picketlink/idm/LdapConstants.java | 5 + .../realm/PartitionManagerRegistry.java | 47 ++++++++-- .../realm/RealmIdentityManagerProvider.java | 12 +-- .../forms/AuthProvidersIntegrationTest.java | 15 ++- .../src/test/resources/ldap/users.ldif | 4 +- 16 files changed, 226 insertions(+), 70 deletions(-) create mode 100644 picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/AbstractIdentityManagerProvider.java delete mode 100644 picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPAgentIgnoreCredentialHandler.java create mode 100644 picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java diff --git a/authentication/authentication-picketlink/src/main/java/org/keycloak/authentication/picketlink/PicketlinkAuthenticationProvider.java b/authentication/authentication-picketlink/src/main/java/org/keycloak/authentication/picketlink/PicketlinkAuthenticationProvider.java index ce69bd3062..615e2c7acf 100755 --- a/authentication/authentication-picketlink/src/main/java/org/keycloak/authentication/picketlink/PicketlinkAuthenticationProvider.java +++ b/authentication/authentication-picketlink/src/main/java/org/keycloak/authentication/picketlink/PicketlinkAuthenticationProvider.java @@ -67,6 +67,10 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider try { User picketlinkUser = new User(username); identityManager.add(picketlinkUser); + + // Hack needed due to ActiveDirectory bug in Picketlink TODO: Remove once https://issues.jboss.org/browse/PLINK-485 fixed and updated in keycloak master + picketlinkUser = BasicModel.getUser(identityManager, picketlinkUser.getLoginName()); + return picketlinkUser.getId(); } catch (IdentityManagementException ie) { throw convertIDMException(ie); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js index 9b95be0daa..fc10a08572 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js @@ -902,6 +902,12 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real module.controller('RealmLdapSettingsCtrl', function($scope, $location, Notifications, Realm, realm) { console.log('RealmLdapSettingsCtrl'); + $scope.ldapVendors = [ + { "id": "ad", "name": "Active Directory" }, + { "id": "rhds", "name": "Red Hat Directory Server" }, + { "id": "other", "name": "Other" } + ]; + $scope.realm = realm; var oldCopy = angular.copy($scope.realm); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-ldap.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-ldap.html index e717ef5b63..6eb9fdb4c8 100644 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-ldap.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-ldap.html @@ -13,6 +13,17 @@
Required Settings +
+ +
+
+ +
+
+
diff --git a/model/tests/src/main/java/org/keycloak/model/test/LDAPEmbeddedServer.java b/model/tests/src/main/java/org/keycloak/model/test/LDAPEmbeddedServer.java index 0598c42154..70c149f6da 100644 --- a/model/tests/src/main/java/org/keycloak/model/test/LDAPEmbeddedServer.java +++ b/model/tests/src/main/java/org/keycloak/model/test/LDAPEmbeddedServer.java @@ -49,6 +49,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { protected boolean startEmbeddedLdapLerver = true; protected String bindDn = "uid=admin,ou=system"; protected String bindCredential = "secret"; + protected String vendor; public static String IDM_TEST_LDAP_CONNECTION_URL = "idm.test.ldap.connection.url"; public static String IDM_TEST_LDAP_BASE_DN = "idm.test.ldap.base.dn"; @@ -59,6 +60,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { public static String IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER = "idm.test.ldap.start.embedded.ldap.server"; public static String IDM_TEST_LDAP_BIND_DN = "idm.test.ldap.bind.dn"; public static String IDM_TEST_LDAP_BIND_CREDENTIAL = "idm.test.ldap.bind.credential"; + public static String IDM_TEST_LDAP_VENDOR = "idm.test.ldap.vendor"; public LDAPEmbeddedServer() { @@ -85,6 +87,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER, "true")); bindDn = p.getProperty(IDM_TEST_LDAP_BIND_DN, bindDn); bindCredential = p.getProperty(IDM_TEST_LDAP_BIND_CREDENTIAL, bindCredential); + vendor = p.getProperty(IDM_TEST_LDAP_VENDOR); } @Override @@ -132,6 +135,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { ldapConfig.put(LdapConstants.BIND_DN, getBindDn()); ldapConfig.put(LdapConstants.BIND_CREDENTIAL, getBindCredential()); ldapConfig.put(LdapConstants.USER_DN_SUFFIX, getUserDnSuffix()); + ldapConfig.put(LdapConstants.VENDOR, getVendor()); realm.setLdapServerConfig(ldapConfig); } @@ -202,6 +206,10 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { return bindCredential; } + public String getVendor() { + return vendor; + } + @Override public void importLDIF(String fileName) throws Exception { // import LDIF only in case we are running against embedded LDAP server diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java index 24317a8c51..cc6eaf1fe4 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java @@ -75,23 +75,23 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { @Test public void testLdapAuthentication() { - MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password"); + MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password"); // Set password of user in LDAP - LdapTestUtils.setLdapPassword(providerSession, realm, "john", "password"); + LdapTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password"); // Verify that user doesn't exists in realm2 and can't authenticate here Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData)); - Assert.assertNull(realm.getUser("john")); + Assert.assertNull(realm.getUser("johnkeycloak")); // Add ldap authenticationProvider setupAuthenticationProviders(); // Authenticate john and verify that now he exists in realm Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); - UserModel john = realm.getUser("john"); + UserModel john = realm.getUser("johnkeycloak"); Assert.assertNotNull(john); - Assert.assertEquals("john", john.getLoginName()); + Assert.assertEquals("johnkeycloak", john.getLoginName()); Assert.assertEquals("John", john.getFirstName()); Assert.assertEquals("Doe", john.getLastName()); Assert.assertEquals("john@email.org", john.getEmail()); @@ -119,7 +119,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData)); // User exists in ldap - formData = AuthProvidersExternalModelTest.createFormData("john", "invalid"); + formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "invalid"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData)); // User exists in realm @@ -142,23 +142,23 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // Add ldap setupAuthenticationProviders(); - LdapTestUtils.setLdapPassword(providerSession, realm, "john", "password"); + LdapTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password"); // First authenticate successfully to sync john into realm - MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password"); + MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); // Change credential and validate that user can authenticate AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm, providerSession); - UserModel john = realm.getUser("john"); + UserModel john = realm.getUser("johnkeycloak"); try { Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); } - formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); + formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password-updated"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); // Password updated just in LDAP, so validating directly in realm should fail @@ -174,7 +174,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { ape.printStackTrace(); Assert.fail("Error not expected"); } - formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated2"); + formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password-updated2"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData)); } diff --git a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index 1c47fd144f..cfd04b5096 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -201,10 +201,11 @@ public class ImportTest extends AbstractModelTest { // Test ldap config Map ldapConfig = realm.getLdapServerConfig(); - Assert.assertTrue(ldapConfig.size() == 5); + Assert.assertTrue(ldapConfig.size() == 6); Assert.assertEquals("ldap://localhost:10389", ldapConfig.get("connectionUrl")); Assert.assertEquals("dc=keycloak,dc=org", ldapConfig.get("baseDn")); Assert.assertEquals("ou=People,dc=keycloak,dc=org", ldapConfig.get("userDnSuffix")); + Assert.assertEquals("other", ldapConfig.get("vendor")); // Test authentication providers List authProviderModels = realm.getAuthenticationProviders(); diff --git a/model/tests/src/test/resources/ldap/users.ldif b/model/tests/src/test/resources/ldap/users.ldif index 8ba3a98564..cd6bac8240 100644 --- a/model/tests/src/test/resources/ldap/users.ldif +++ b/model/tests/src/test/resources/ldap/users.ldif @@ -9,12 +9,12 @@ objectclass: top objectclass: organizationalUnit ou: People -dn: uid=john,ou=People,dc=keycloak,dc=org +dn: uid=johnkeycloak,ou=People,dc=keycloak,dc=org objectclass: top objectclass: uidObject objectclass: person objectclass: inetOrgPerson -uid: john +uid: johnkeycloak cn: John sn: Doe mail: john@email.org diff --git a/model/tests/src/test/resources/testrealm.json b/model/tests/src/test/resources/testrealm.json index 56bf23ee71..4ab4ebf15e 100755 --- a/model/tests/src/test/resources/testrealm.json +++ b/model/tests/src/test/resources/testrealm.json @@ -17,7 +17,8 @@ "baseDn": "dc=keycloak,dc=org", "userDnSuffix": "ou=People,dc=keycloak,dc=org", "bindDn": "uid=admin,ou=system", - "bindCredential": "secret" + "bindCredential": "secret", + "vendor": "other" }, "socialProviders": { "google.key": "abc", diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/AbstractIdentityManagerProvider.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/AbstractIdentityManagerProvider.java new file mode 100644 index 0000000000..3ee589e343 --- /dev/null +++ b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/AbstractIdentityManagerProvider.java @@ -0,0 +1,32 @@ +package org.keycloak.picketlink; + +import org.keycloak.models.RealmModel; +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.PartitionManager; + +/** + * Per-request IdentityManager caching . Not thread-safe + * + * @author Marek Posolda + */ +public abstract class AbstractIdentityManagerProvider implements IdentityManagerProvider { + + private IdentityManager identityManager; + + @Override + public IdentityManager getIdentityManager(RealmModel realm) { + if (identityManager == null) { + PartitionManager partitionManager = getPartitionManager(realm); + identityManager = partitionManager.createIdentityManager(); + } + + return identityManager; + } + + protected abstract PartitionManager getPartitionManager(RealmModel realm); + + @Override + public void close() { + identityManager = null; + } +} diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPAgentIgnoreCredentialHandler.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPAgentIgnoreCredentialHandler.java deleted file mode 100644 index 4bbc73b1bd..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPAgentIgnoreCredentialHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.keycloak.picketlink.idm; - -import org.picketlink.idm.IdentityManager; -import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler; -import org.picketlink.idm.model.Account; -import org.picketlink.idm.model.basic.User; -import org.picketlink.idm.spi.IdentityContext; - -import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER; -import static org.picketlink.idm.model.basic.BasicModel.getUser; - -/** - * @author Marek Posolda - */ -public class LDAPAgentIgnoreCredentialHandler extends LDAPPlainTextPasswordCredentialHandler { - - @Override - protected Account getAccount(IdentityContext context, String loginName) { - IdentityManager identityManager = getIdentityManager(context); - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Trying to find account [%s] using default account type [%s]", loginName, User.class); - } - - return getUser(identityManager, loginName); - } -} diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java new file mode 100644 index 0000000000..62c64f33a5 --- /dev/null +++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java @@ -0,0 +1,93 @@ +package org.keycloak.picketlink.idm; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Date; + +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; + +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.ldap.internal.LDAPIdentityStore; +import org.picketlink.idm.ldap.internal.LDAPOperationManager; +import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler; +import org.picketlink.idm.model.Account; +import org.picketlink.idm.model.AttributedType; +import org.picketlink.idm.model.basic.User; +import org.picketlink.idm.spi.IdentityContext; + +import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER; +import static org.picketlink.idm.model.basic.BasicModel.getUser; + +/** + * @author Marek Posolda + */ +public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredentialHandler { + + private static Method GET_BINDING_DN_METHOD; + private static Method GET_OPERATION_MANAGER_METHOD; + + static { + GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class); + GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager"); + } + + // Used just in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored + private String userAccountControlAfterPasswordUpdate; + + @Override + public void setup(LDAPIdentityStore store) { + if (store.getConfig().isActiveDirectory() || Boolean.getBoolean("keycloak.ldap.ad.skipUserAccountControlAfterPasswordUpdate")) { + String userAccountControlProp = System.getProperty("keycloak.ldap.ad.userAccountControlAfterPasswordUpdate"); + this.userAccountControlAfterPasswordUpdate = userAccountControlProp!=null ? userAccountControlProp : "512"; + CREDENTIAL_LOGGER.info("Will use userAccountControl=" + userAccountControlAfterPasswordUpdate + " after password update of user in Active Directory"); + } + } + + // Overridden as in Keycloak, we don't have Agents + @Override + protected Account getAccount(IdentityContext context, String loginName) { + IdentityManager identityManager = getIdentityManager(context); + + if (CREDENTIAL_LOGGER.isDebugEnabled()) { + CREDENTIAL_LOGGER.debugf("Trying to find account [%s] using default account type [%s]", loginName, User.class); + } + + return getUser(identityManager, loginName); + } + + @Override + public void update(IdentityContext context, Account account, Password password, LDAPIdentityStore store, Date effectiveDate, Date expiryDate) { + super.update(context, account, password, store, effectiveDate, expiryDate); + + if (userAccountControlAfterPasswordUpdate != null) { + ModificationItem[] mods = new ModificationItem[1]; + BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate); + mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0); + + try { + String bindingDN = (String) GET_BINDING_DN_METHOD.invoke(store, account); + LDAPOperationManager operationManager = (LDAPOperationManager) GET_OPERATION_MANAGER_METHOD.invoke(store); + operationManager.modifyAttribute(bindingDN, mod0); + } catch (IllegalAccessException iae) { + throw new RuntimeException(iae); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getTargetException() != null ? ite.getTargetException() : ite; + throw new RuntimeException(cause); + } + } + } + + // Hack as methods are protected on LDAPIdentityStore :/ + private static Method getMethodOnLDAPStore(String methodName, Class... classes) { + try { + Method m = LDAPIdentityStore.class.getDeclaredMethod(methodName, classes); + m.setAccessible(true); + return m; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LdapConstants.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LdapConstants.java index d7fbd12d0d..4eb3454900 100644 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LdapConstants.java +++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LdapConstants.java @@ -5,6 +5,11 @@ package org.keycloak.picketlink.idm; */ public class LdapConstants { + public static final String VENDOR = "vendor"; + public static final String VENDOR_RHDS = "rhds"; + public static final String VENDOR_ACTIVE_DIRECTORY = "ad"; + public static final String VENDOR_OTHER = "other"; + public static final String CONNECTION_URL = "connectionUrl"; public static final String BASE_DN = "baseDn"; public static final String USER_DN_SUFFIX = "userDnSuffix"; diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java index 261fdb345e..6204aa7704 100644 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java +++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java @@ -6,15 +6,15 @@ import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; import org.keycloak.models.RealmModel; -import org.keycloak.picketlink.idm.LDAPAgentIgnoreCredentialHandler; +import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler; import org.keycloak.picketlink.idm.LdapConstants; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.config.IdentityConfigurationBuilder; +import org.picketlink.idm.config.LDAPIdentityStoreConfiguration; import org.picketlink.idm.internal.DefaultPartitionManager; import org.picketlink.idm.model.basic.User; import static org.picketlink.common.constants.LDAPConstants.CN; -import static org.picketlink.common.constants.LDAPConstants.CREATE_TIMESTAMP; import static org.picketlink.common.constants.LDAPConstants.EMAIL; import static org.picketlink.common.constants.LDAPConstants.SN; import static org.picketlink.common.constants.LDAPConstants.UID; @@ -39,8 +39,8 @@ public class PartitionManagerRegistry { // Ldap config might have changed for the realm. In this case, we must re-initialize if (context == null || !ldapConfig.equals(context.config)) { - logger.infof("Creating new partition manager for the realm: %s, LDAP Connection URL: %s, LDAP Base DN: %s", realm.getId(), - ldapConfig.get(LdapConstants.CONNECTION_URL), ldapConfig.get(LdapConstants.BASE_DN)); + logger.infof("Creating new partition manager for the realm: %s, LDAP Connection URL: %s, LDAP Base DN: %s, LDAP Vendor: %s", realm.getId(), + ldapConfig.get(LdapConstants.CONNECTION_URL), ldapConfig.get(LdapConstants.BASE_DN), ldapConfig.get(LdapConstants.VENDOR)); PartitionManager manager = createPartitionManager(ldapConfig); context = new PartitionManagerContext(ldapConfig, manager); partitionManagers.put(realm.getId(), context); @@ -66,26 +66,43 @@ public class PartitionManagerRegistry { checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain"); checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off"); + String vendor = ldapConfig.get(LdapConstants.VENDOR); + + // RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID" + if (vendor != null && vendor.equals(LdapConstants.VENDOR_RHDS)) { + checkSystemProperty(LDAPIdentityStoreConfiguration.ENTRY_IDENTIFIER_ATTRIBUTE_NAME, "nsuniqueid"); + } + + boolean activeDirectory = vendor != null && vendor.equals(LdapConstants.VENDOR_ACTIVE_DIRECTORY); + + // Try to compute properties based on LDAP server type, but still allow to override them through System properties TODO: Should allow better way than overriding from System properties. Perhaps init from XML? + String ldapLoginName = getNameOfLDAPAttribute("keycloak.ldap.idm.loginName", UID, CN, activeDirectory); + String ldapFirstName = getNameOfLDAPAttribute("keycloak.ldap.idm.firstName", CN, "givenName", activeDirectory); + String ldapLastName = getNameOfLDAPAttribute("keycloak.ldap.idm.lastName", SN, SN, activeDirectory); + String ldapEmail = getNameOfLDAPAttribute("keycloak.ldap.idm.email", EMAIL, EMAIL, activeDirectory); + + logger.infof("LDAP Attributes mapping: loginName: %s, firstName: %s, lastName: %s, email: %s", ldapLoginName, ldapFirstName, ldapLastName, ldapEmail); + // Use same mapping for User and Agent for now builder .named("SIMPLE_LDAP_STORE_CONFIG") .stores() .ldap() .connectionProperties(connectionProps) - .addCredentialHandler(LDAPAgentIgnoreCredentialHandler.class) + .addCredentialHandler(LDAPKeycloakCredentialHandler.class) .baseDN(ldapConfig.get(LdapConstants.BASE_DN)) .bindDN(ldapConfig.get(LdapConstants.BIND_DN)) .bindCredential(ldapConfig.get(LdapConstants.BIND_CREDENTIAL)) .url(ldapConfig.get(LdapConstants.CONNECTION_URL)) + .activeDirectory(activeDirectory) .supportAllFeatures() .mapping(User.class) .baseDN(ldapConfig.get(LdapConstants.USER_DN_SUFFIX)) .objectClasses("inetOrgPerson", "organizationalPerson") - .attribute("loginName", UID, true) - .attribute("firstName", CN) - .attribute("lastName", SN) - .attribute("email", EMAIL) - .readOnlyAttribute("createdDate", CREATE_TIMESTAMP); + .attribute("loginName", ldapLoginName, true) + .attribute("firstName", ldapFirstName) + .attribute("lastName", ldapLastName) + .attribute("email", ldapEmail); return new DefaultPartitionManager(builder.buildAll()); } @@ -96,6 +113,16 @@ public class PartitionManagerRegistry { } } + private String getNameOfLDAPAttribute(String systemPropertyName, String defaultAttrName, String defaultAttrNameInActiveDirectory, boolean activeDirectory) { + // System property has biggest priority if available + String sysProperty = System.getProperty(systemPropertyName); + if (sysProperty != null) { + return sysProperty; + } + + return activeDirectory ? defaultAttrNameInActiveDirectory : defaultAttrName; + } + private class PartitionManagerContext { private PartitionManagerContext(Map config, PartitionManager manager) { diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProvider.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProvider.java index 3c00489c30..849aa98e3b 100644 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProvider.java +++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProvider.java @@ -1,6 +1,7 @@ package org.keycloak.picketlink.realm; import org.keycloak.models.RealmModel; +import org.keycloak.picketlink.AbstractIdentityManagerProvider; import org.keycloak.picketlink.IdentityManagerProvider; import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; @@ -8,7 +9,7 @@ import org.picketlink.idm.PartitionManager; /** * @author Marek Posolda */ -public class RealmIdentityManagerProvider implements IdentityManagerProvider { +public class RealmIdentityManagerProvider extends AbstractIdentityManagerProvider { private final PartitionManagerRegistry partitionManagerRegistry; @@ -17,12 +18,7 @@ public class RealmIdentityManagerProvider implements IdentityManagerProvider { } @Override - public IdentityManager getIdentityManager(RealmModel realm) { - PartitionManager partitionManager = partitionManagerRegistry.getPartitionManager(realm); - return partitionManager.createIdentityManager(); - } - - @Override - public void close() { + protected PartitionManager getPartitionManager(RealmModel realm) { + return partitionManagerRegistry.getPartitionManager(realm); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java index cd367c9bba..839813c0de 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java @@ -62,7 +62,7 @@ public class AuthProvidersIntegrationTest { // Configure LDAP ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm); - LdapTestUtils.setLdapPassword(providerSession, appRealm, "john", "password"); + LdapTestUtils.setLdapPassword(providerSession, appRealm, "johnkeycloak", "password"); } }); @@ -135,7 +135,7 @@ public class AuthProvidersIntegrationTest { @Test public void loginLdap() { loginPage.open(); - loginPage.login("john", "password"); + loginPage.login("johnkeycloak", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -186,20 +186,19 @@ public class AuthProvidersIntegrationTest { @Test public void passwordChangeLdap() throws Exception { changePasswordPage.open(); - loginPage.login("john", "password"); + loginPage.login("johnkeycloak", "password"); changePasswordPage.changePassword("password", "new-password", "new-password"); Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); changePasswordPage.logout(); -// TODO: Disabled until https://issues.jboss.org/browse/PLINK-384 is released and updated -// loginPage.open(); -// loginPage.login("john", "password"); -// Assert.assertEquals("Invalid username or password.", loginPage.getError()); + loginPage.open(); + loginPage.login("johnkeycloak", "password"); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); loginPage.open(); - loginPage.login("john", "new-password"); + loginPage.login("johnkeycloak", "new-password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); } diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif index 0debe0bec0..386a9d6f3c 100644 --- a/testsuite/integration/src/test/resources/ldap/users.ldif +++ b/testsuite/integration/src/test/resources/ldap/users.ldif @@ -19,12 +19,12 @@ objectclass: top objectclass: organizationalUnit ou: Groups -dn: uid=john,ou=People,dc=keycloak,dc=org +dn: uid=johnkeycloak,ou=People,dc=keycloak,dc=org objectclass: top objectclass: uidObject objectclass: person objectclass: inetOrgPerson -uid: john +uid: johnkeycloak cn: John sn: Doe mail: john@email.org