diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index d36543dd63..92f6df07b8 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -112,6 +112,11 @@ keycloak-picketlink-api ${project.version} + + org.keycloak + keycloak-picketlink-ldap + ${project.version} + diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index 0ce4c2e9c5..a156a54088 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -25,6 +25,12 @@ ${project.version} provided + + org.keycloak + keycloak-picketlink-api + ${project.version} + provided + org.jboss.resteasy resteasy-jaxrs diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/KeycloakLDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/KeycloakLDAPIdentityStore.java deleted file mode 100755 index 1ff6c119a1..0000000000 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/KeycloakLDAPIdentityStore.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.keycloak.federation.ldap; - -import org.keycloak.models.utils.reflection.Reflections; -import org.picketlink.idm.config.LDAPMappingConfiguration; -import org.picketlink.idm.ldap.internal.LDAPIdentityStore; -import org.picketlink.idm.ldap.internal.LDAPOperationManager; -import org.picketlink.idm.model.AttributedType; -import org.picketlink.idm.model.basic.User; -import org.picketlink.idm.query.IdentityQuery; -import org.picketlink.idm.spi.IdentityContext; - -import javax.naming.directory.BasicAttributes; -import java.lang.reflect.Method; - -import static org.picketlink.common.constants.LDAPConstants.*; - -/** - * @author Marek Posolda - */ -public class KeycloakLDAPIdentityStore extends LDAPIdentityStore { - - public static Method GET_OPERATION_MANAGER_METHOD; - public static Method CREATE_SEARCH_FILTER_METHOD; - public static Method EXTRACT_ATTRIBUTES_METHOD; - public static Method GET_ENTRY_IDENTIFIER_METHOD; - - public static final String SAM_ACCOUNT_NAME = "sAMAccountName"; - - static { - GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager"); - CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class); - EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class); - GET_ENTRY_IDENTIFIER_METHOD = getMethodOnLDAPStore("getEntryIdentifier", AttributedType.class); - } - - @Override - public void addAttributedType(IdentityContext context, AttributedType attributedType) { - LDAPMappingConfiguration userMappingConfig = getConfig().getMappingConfig(attributedType.getClass()); - String ldapUsernameAttrName = userMappingConfig.getMappedProperties().get(userMappingConfig.getIdProperty().getName()); - - if (getConfig().isActiveDirectory() && SAM_ACCOUNT_NAME.equals(ldapUsernameAttrName)) { - // TODO: pain, but everything in picketlink is private... Improve if possible... - LDAPOperationManager operationManager = Reflections.invokeMethod(false, GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, this); - - String cn = getCommonName(attributedType); - String bindingDN = CN + EQUAL + cn + COMMA + userMappingConfig.getBaseDN(); - - BasicAttributes ldapAttributes = Reflections.invokeMethod(false, EXTRACT_ATTRIBUTES_METHOD, BasicAttributes.class, this, attributedType, true); - ldapAttributes.put(CN, cn); - - operationManager.createSubContext(bindingDN, ldapAttributes); - - String ldapId = Reflections.invokeMethod(false, GET_ENTRY_IDENTIFIER_METHOD, String.class, this, attributedType); - attributedType.setId(ldapId); - } else { - super.addAttributedType(context, attributedType); - } - } - - // Hack as methods are protected on LDAPIdentityStore :/ - public 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); - } - } - - protected String getCommonName(AttributedType aType) { - User user = (User)aType; - String fullName; - if (user.getFirstName() != null && user.getLastName() != null) { - fullName = user.getFirstName() + " " + user.getLastName(); - } else if (user.getFirstName() != null && user.getFirstName().trim().length() > 0) { - fullName = user.getFirstName(); - } else { - fullName = user.getLastName(); - } - - // Fallback to loginName - if (fullName == null || fullName.trim().length() == 0) { - fullName = user.getLoginName(); - } - - return fullName; - } -} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 27a6dd2c82..562e247715 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -16,6 +16,7 @@ import org.picketlink.idm.PartitionManager; import org.picketlink.idm.credential.Credentials; import org.picketlink.idm.credential.Password; import org.picketlink.idm.credential.UsernamePasswordCredentials; +import org.picketlink.idm.model.Attribute; import org.picketlink.idm.model.basic.BasicModel; import org.picketlink.idm.model.basic.User; import org.picketlink.idm.query.IdentityQuery; @@ -123,6 +124,7 @@ public class LDAPFederationProvider implements UserFederationProvider { picketlinkUser.setFirstName(user.getFirstName()); picketlinkUser.setLastName(user.getLastName()); picketlinkUser.setEmail(user.getEmail()); + picketlinkUser.setAttribute(new Attribute("fullName", getFullName(user))); identityManager.add(picketlinkUser); user.setAttribute(LDAP_ID, picketlinkUser.getId()); return proxy(user); @@ -321,4 +323,23 @@ public class LDAPFederationProvider implements UserFederationProvider { public void close() { //To change body of implemented methods use File | Settings | File Templates. } + + // Needed for ActiveDirectory updates + protected String getFullName(UserModel user) { + String fullName; + if (user.getFirstName() != null && user.getLastName() != null) { + fullName = user.getFirstName() + " " + user.getLastName(); + } else if (user.getFirstName() != null && user.getFirstName().trim().length() > 0) { + fullName = user.getFirstName(); + } else { + fullName = user.getLastName(); + } + + // Fallback to loginName + if (fullName == null || fullName.trim().length() == 0) { + fullName = user.getUsername(); + } + + return fullName; + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 8da274e916..ad6d18508e 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -5,10 +5,10 @@ import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.picketlink.PartitionManagerProvider; import org.picketlink.idm.PartitionManager; import java.util.Collections; -import java.util.List; import java.util.Set; /** @@ -18,7 +18,6 @@ import java.util.Set; */ public class LDAPFederationProviderFactory implements UserFederationProviderFactory { public static final String PROVIDER_NAME = "ldap"; - PartitionManagerRegistry registry; @Override public UserFederationProvider create(KeycloakSession session) { @@ -27,13 +26,13 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact @Override public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { - PartitionManager partition = registry.getPartitionManager(model); + PartitionManagerProvider idmProvider = session.getProvider(PartitionManagerProvider.class); + PartitionManager partition = idmProvider.getPartitionManager(model); return new LDAPFederationProvider(session, model, partition); } @Override public void init(Config.Scope config) { - registry = new PartitionManagerRegistry(); } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPKeycloakCredentialHandler.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPKeycloakCredentialHandler.java deleted file mode 100755 index 7078d87494..0000000000 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPKeycloakCredentialHandler.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.keycloak.federation.ldap; - -import org.keycloak.models.utils.reflection.Reflections; -import org.picketlink.idm.IdentityManager; -import org.picketlink.idm.config.LDAPMappingConfiguration; -import org.picketlink.idm.credential.Credentials; -import org.picketlink.idm.credential.Password; -import org.picketlink.idm.credential.UsernamePasswordCredentials; -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.basic.User; -import org.picketlink.idm.query.IdentityQuery; -import org.picketlink.idm.spi.IdentityContext; - -import javax.naming.NamingException; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.DirContext; -import javax.naming.directory.ModificationItem; -import javax.naming.directory.SearchResult; -import java.io.UnsupportedEncodingException; -import java.util.Date; -import java.util.List; - -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 { - - // 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) { - // TODO: Don't setup it here once PLINK-508 is fixed - 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 User 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) { - if (!store.getConfig().isActiveDirectory()) { - super.update(context, account, password, store, effectiveDate, expiryDate); - } else { - User user = (User)account; - LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store); - IdentityManager identityManager = getIdentityManager(context); - String userDN = getDNOfUser(user, identityManager, store, operationManager); - - updateADPassword(userDN, new String(password.getValue()), operationManager); - - if (userAccountControlAfterPasswordUpdate != null) { - ModificationItem[] mods = new ModificationItem[1]; - BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate); - mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0); - operationManager.modifyAttribute(userDN, mod0); - } - } - } - - protected void updateADPassword(String userDN, String password, LDAPOperationManager operationManager) { - 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); - operationManager.modifyAttribute(userDN, unicodePwd); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - @Override - public void validate(IdentityContext context, UsernamePasswordCredentials credentials, - LDAPIdentityStore store) { - credentials.setStatus(Credentials.Status.INVALID); - credentials.setValidatedAccount(null); - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Validating credentials [%s][%s] using identity store [%s] and credential handler [%s].", credentials.getClass(), credentials, store, this); - } - - User account = getAccount(context, credentials.getUsername()); - - // If the user for the provided username cannot be found we fail validation - if (account != null) { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Found account [%s] from credentials [%s].", account, credentials); - } - - if (account.isEnabled()) { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account [%s] is ENABLED.", account, credentials); - } - - char[] password = credentials.getPassword().getValue(); - - // String bindingDN = store.getBindingDN(account); - - LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store); - String bindingDN = getDNOfUser(account, getIdentityManager(context), store, operationManager); - - if (operationManager.authenticate(bindingDN, new String(password))) { - credentials.setValidatedAccount(account); - credentials.setStatus(Credentials.Status.VALID); - } - } else { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account [%s] is DISABLED.", account, credentials); - } - credentials.setStatus(Credentials.Status.ACCOUNT_DISABLED); - } - } else { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account NOT FOUND for credentials [%s][%s].", credentials.getClass(), credentials); - } - } - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Credential [%s][%s] validated using identity store [%s] and credential handler [%s]. Status [%s]. Validated Account [%s]", - credentials.getClass(), credentials, store, this, credentials.getStatus(), credentials.getValidatedAccount()); - } - } - - // TODO: remove later... It's needed just because LDAPIdentityStore.getBindingDN, which always uses idProperty as first part of DN, but in AD it doesn't work as we may have idProperty 'sAMAccountName' - // but DN like: cn=John Doe,OU=foo,DC=bar - protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) { - - LDAPMappingConfiguration ldapEntryConfig = ldapStore.getConfig().getMappingConfig(User.class); - IdentityQuery identityQuery = identityManager.createIdentityQuery(User.class) - .setParameter(User.LOGIN_NAME, user.getLoginName()); - StringBuilder filter = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.CREATE_SEARCH_FILTER_METHOD, StringBuilder.class, ldapStore, identityQuery, ldapEntryConfig); - - List search = null; - try { - search = operationManager.search(ldapEntryConfig.getBaseDN(), filter.toString(), ldapEntryConfig); - } catch (NamingException ne) { - throw new RuntimeException(ne); - } - - if (search.size() > 0) { - SearchResult sr1 = search.get(0); - return sr1.getNameInNamespace(); - } else { - return null; - } - } -} diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java index 3b6ac1c092..ba29870729 100644 --- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java +++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java @@ -18,4 +18,6 @@ public class LDAPConstants { public static final String USER_DN_SUFFIX = "userDnSuffix"; public static final String BIND_DN = "bindDn"; public static final String BIND_CREDENTIAL = "bindCredential"; + + public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate"; } 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 deleted file mode 100644 index 3ee589e343..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/AbstractIdentityManagerProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -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-api/src/main/java/org/keycloak/picketlink/IdentityManagerProvider.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerProvider.java deleted file mode 100644 index 4b9ec6e3dd..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.keycloak.picketlink; - -import org.keycloak.models.RealmModel; -import org.keycloak.provider.Provider; -import org.picketlink.idm.IdentityManager; - -/** - * @author Marek Posolda - */ -public interface IdentityManagerProvider extends Provider { - - IdentityManager getIdentityManager(RealmModel realm); -} diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java new file mode 100644 index 0000000000..0b22305364 --- /dev/null +++ b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java @@ -0,0 +1,14 @@ +package org.keycloak.picketlink; + +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.provider.Provider; +import org.picketlink.idm.PartitionManager; + +/** + * + * @author Marek Posolda + */ +public interface PartitionManagerProvider extends Provider { + + PartitionManager getPartitionManager(UserFederationProviderModel model); +} diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerProviderFactory.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java similarity index 60% rename from picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerProviderFactory.java rename to picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java index 1456e98e09..203c7f9c23 100644 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerProviderFactory.java +++ b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java @@ -5,5 +5,5 @@ import org.keycloak.provider.ProviderFactory; /** * @author Marek Posolda */ -public interface IdentityManagerProviderFactory extends ProviderFactory { +public interface PartitionManagerProviderFactory extends ProviderFactory { } diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerSpi.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java similarity index 70% rename from picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerSpi.java rename to picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java index 42111da0d3..721cf12d68 100644 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/IdentityManagerSpi.java +++ b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java @@ -7,19 +7,19 @@ import org.keycloak.provider.Spi; /** * @author Stian Thorgersen */ -public class IdentityManagerSpi implements Spi { +public class PartitionManagerSpi implements Spi { @Override public String getName() { - return "picketlink-identity-manager"; + return "picketlink-idm"; } @Override public Class getProviderClass() { - return IdentityManagerProvider.class; + return PartitionManagerProvider.class; } @Override public Class getProviderFactoryClass() { - return IdentityManagerProviderFactory.class; + return PartitionManagerProviderFactory.class; } } diff --git a/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi index ffcb6e5424..88b1bcc076 100644 --- a/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -1 +1 @@ -org.keycloak.picketlink.IdentityManagerSpi \ No newline at end of file +org.keycloak.picketlink.PartitionManagerSpi \ No newline at end of file diff --git a/picketlink/keycloak-picketlink-realm/pom.xml b/picketlink/keycloak-picketlink-ldap/pom.xml similarity index 95% rename from picketlink/keycloak-picketlink-realm/pom.xml rename to picketlink/keycloak-picketlink-ldap/pom.xml index d95eaaabc5..9360aa81d8 100755 --- a/picketlink/keycloak-picketlink-realm/pom.xml +++ b/picketlink/keycloak-picketlink-ldap/pom.xml @@ -10,8 +10,8 @@ 4.0.0 - keycloak-picketlink-realm - Keycloak Picketlink Realm + keycloak-picketlink-ldap + Keycloak Picketlink LDAP diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java new file mode 100644 index 0000000000..90a221eefb --- /dev/null +++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java @@ -0,0 +1,57 @@ +package org.keycloak.picketlink.idm; + +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; + +import org.jboss.logging.Logger; +import org.picketlink.idm.PartitionManager; +import org.picketlink.idm.event.CredentialUpdatedEvent; +import org.picketlink.idm.event.EventBridge; +import org.picketlink.idm.ldap.internal.LDAPIdentityStore; +import org.picketlink.idm.ldap.internal.LDAPOperationManager; +import org.picketlink.idm.model.basic.User; +import org.picketlink.idm.spi.CredentialStore; +import org.picketlink.idm.spi.IdentityContext; +import org.picketlink.idm.spi.StoreSelector; + +/** + * @author Marek Posolda + */ +public class KeycloakEventBridge implements EventBridge { + + private static final Logger logger = Logger.getLogger(KeycloakEventBridge.class); + + private final boolean updateUserAccountAfterPasswordUpdate; + + public KeycloakEventBridge(boolean updateUserAccountAfterPasswordUpdate) { + this.updateUserAccountAfterPasswordUpdate = updateUserAccountAfterPasswordUpdate; + } + + @Override + public void raiseEvent(Object event) { + // Used 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 + if (updateUserAccountAfterPasswordUpdate && event instanceof CredentialUpdatedEvent) { + CredentialUpdatedEvent credEvent = ((CredentialUpdatedEvent) event); + PartitionManager partitionManager = credEvent.getPartitionMananger(); + IdentityContext identityCtx = (IdentityContext)partitionManager.createIdentityManager(); + + CredentialStore store = ((StoreSelector)partitionManager).getStoreForCredentialOperation(identityCtx, credEvent.getCredential().getClass()); + if (store instanceof LDAPIdentityStore) { + LDAPIdentityStore ldapStore = (LDAPIdentityStore)store; + LDAPOperationManager operationManager = ldapStore.getOperationManager(); + User picketlinkUser = (User) credEvent.getAccount(); + String userDN = ldapStore.getBindingDN(picketlinkUser, true); + + ModificationItem[] mods = new ModificationItem[1]; + BasicAttribute mod0 = new BasicAttribute("userAccountControl", "512"); + mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0); + operationManager.modifyAttribute(userDN, mod0); + logger.debug("Attribute userAccountControls switched to 512 after password update of user " + picketlinkUser.getLoginName()); + } else { + logger.debug("Store for credential updates is not LDAPIdentityStore. Ignored"); + } + + } + } +} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java new file mode 100644 index 0000000000..7844e8e19f --- /dev/null +++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java @@ -0,0 +1,27 @@ +package org.keycloak.picketlink.idm; + +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler; +import org.picketlink.idm.model.basic.BasicModel; +import org.picketlink.idm.model.basic.User; +import org.picketlink.idm.spi.IdentityContext; + +import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER; + +/** + * @author Marek Posolda + */ +public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredentialHandler { + + // Overridden as in Keycloak, we don't have Agents + @Override + protected User 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 BasicModel.getUser(identityManager, loginName); + } +} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java new file mode 100644 index 0000000000..bbb7201e69 --- /dev/null +++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java @@ -0,0 +1,26 @@ +package org.keycloak.picketlink.ldap; + +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.picketlink.PartitionManagerProvider; +import org.picketlink.idm.PartitionManager; + +/** + * @author Marek Posolda + */ +public class LDAPPartitionManagerProvider implements PartitionManagerProvider { + + private final PartitionManagerRegistry partitionManagerRegistry; + + public LDAPPartitionManagerProvider(PartitionManagerRegistry partitionManagerRegistry) { + this.partitionManagerRegistry = partitionManagerRegistry; + } + + @Override + public PartitionManager getPartitionManager(UserFederationProviderModel model) { + return partitionManagerRegistry.getPartitionManager(model); + } + + @Override + public void close() { + } +} diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProviderFactory.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java similarity index 54% rename from picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProviderFactory.java rename to picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java index 616e96d43e..b647057e46 100644 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProviderFactory.java +++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java @@ -1,23 +1,23 @@ -package org.keycloak.picketlink.realm; +package org.keycloak.picketlink.ldap; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; -import org.keycloak.picketlink.IdentityManagerProvider; -import org.keycloak.picketlink.IdentityManagerProviderFactory; +import org.keycloak.picketlink.PartitionManagerProvider; +import org.keycloak.picketlink.PartitionManagerProviderFactory; import org.picketlink.idm.PartitionManager; /** - * Obtains {@link PartitionManager} instances from shared {@link PartitionManagerRegistry} and uses realm configuration for it + * Obtains {@link PartitionManager} instances from shared {@link PartitionManagerRegistry} and uses UserFederationModel configuration for it * * @author Marek Posolda */ -public class RealmIdentityManagerProviderFactory implements IdentityManagerProviderFactory { +public class LDAPPartitionManagerProviderFactory implements PartitionManagerProviderFactory { private PartitionManagerRegistry partitionManagerRegistry; @Override - public IdentityManagerProvider create(KeycloakSession session) { - return new RealmIdentityManagerProvider(partitionManagerRegistry); + public PartitionManagerProvider create(KeycloakSession session) { + return new LDAPPartitionManagerProvider(partitionManagerRegistry); } @Override @@ -31,7 +31,7 @@ public class RealmIdentityManagerProviderFactory implements IdentityManagerProvi @Override public String getId() { - return "realm"; + return "ldap"; } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/PartitionManagerRegistry.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java old mode 100755 new mode 100644 similarity index 61% rename from federation/ldap/src/main/java/org/keycloak/federation/ldap/PartitionManagerRegistry.java rename to picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java index 9992b877de..53e4cbe68b --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/PartitionManagerRegistry.java +++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java @@ -1,23 +1,26 @@ -package org.keycloak.federation.ldap; +package org.keycloak.picketlink.ldap; -import org.jboss.logging.Logger; -import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.LDAPConstants; -import org.picketlink.idm.PartitionManager; -import org.picketlink.idm.config.AbstractIdentityStoreConfiguration; -import org.picketlink.idm.config.IdentityConfiguration; -import org.picketlink.idm.config.IdentityConfigurationBuilder; -import org.picketlink.idm.config.IdentityStoreConfiguration; -import org.picketlink.idm.config.LDAPIdentityStoreConfiguration; -import org.picketlink.idm.internal.DefaultPartitionManager; -import org.picketlink.idm.model.basic.User; - -import java.util.List; +import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; -import static org.picketlink.common.constants.LDAPConstants.*; +import org.jboss.logging.Logger; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.picketlink.idm.KeycloakEventBridge; +import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler; +import org.picketlink.idm.PartitionManager; +import org.picketlink.idm.config.IdentityConfigurationBuilder; +import org.picketlink.idm.config.LDAPMappingConfigurationBuilder; +import org.picketlink.idm.config.LDAPStoreConfigurationBuilder; +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.EMAIL; +import static org.picketlink.common.constants.LDAPConstants.SN; +import static org.picketlink.common.constants.LDAPConstants.UID; /** * @author Marek Posolda @@ -34,8 +37,8 @@ public class PartitionManagerRegistry { // Ldap config might have changed for the realm. In this case, we must re-initialize Map config = model.getConfig(); if (context == null || !config.equals(context.config)) { - logger.infof("Creating new partition manager for the federation provider: %s, LDAP Connection URL: %s, LDAP Base DN: %s, LDAP Vendor: %s", model.getId(), - config.get(LDAPConstants.CONNECTION_URL), config.get(LDAPConstants.BASE_DN), config.get(LDAPConstants.VENDOR)); + logLDAPConfig(model.getId(), config); + PartitionManager manager = createPartitionManager(config); context = new PartitionManagerContext(config, manager); partitionManagers.put(model.getId(), context); @@ -43,6 +46,13 @@ public class PartitionManagerRegistry { return context.partitionManager; } + // Don't log LDAP password + private void logLDAPConfig(String fedProviderId, Map ldapConfig) { + Map copy = new HashMap(ldapConfig); + copy.remove(LDAPConstants.BIND_CREDENTIAL); + logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderId + ", LDAP Configuration: " + copy); + } + /** * @param ldapConfig from realm * @return PartitionManager instance based on LDAP store @@ -55,7 +65,7 @@ public class PartitionManagerRegistry { 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", "10"); + 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"); @@ -63,11 +73,6 @@ public class PartitionManagerRegistry { 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); String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE); @@ -75,17 +80,11 @@ public class PartitionManagerRegistry { ldapLoginNameMapping = activeDirectory ? CN : UID; } - // 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? - ldapLoginNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.loginName", ldapLoginNameMapping, ldapLoginNameMapping, activeDirectory); - String ldapFirstNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.firstName", CN, "givenName", activeDirectory); - String ldapLastNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.lastName", SN, SN, activeDirectory); - String ldapEmailMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.email", EMAIL, EMAIL, activeDirectory); - + String ldapFirstNameMapping = activeDirectory ? "givenName" : CN; String[] userObjectClasses = getUserObjectClasses(ldapConfig); - logger.infof("LDAP Attributes mapping: loginName: %s, firstName: %s, lastName: %s, email: %s", ldapLoginNameMapping, ldapFirstNameMapping, ldapLastNameMapping, ldapEmailMapping); - // Use same mapping for User and Agent for now + LDAPStoreConfigurationBuilder ldapStoreBuilder = builder .named("SIMPLE_LDAP_STORE_CONFIG") .stores() @@ -97,21 +96,29 @@ public class PartitionManagerRegistry { .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(userObjectClasses) - .attribute("loginName", ldapLoginNameMapping, true) - .attribute("firstName", ldapFirstNameMapping) - .attribute("lastName", ldapLastNameMapping) - .attribute("email", ldapEmailMapping); + .supportAllFeatures(); - // Workaround to override the LDAPIdentityStore with our own :/ - List identityConfigs = builder.buildAll(); - IdentityStoreConfiguration identityStoreConfig = identityConfigs.get(0).getStoreConfiguration().get(0); - ((AbstractIdentityStoreConfiguration)identityStoreConfig).setIdentityStoreType(KeycloakLDAPIdentityStore.class); + // RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID" + if (vendor != null && vendor.equals(LDAPConstants.VENDOR_RHDS)) { + ldapStoreBuilder.uniqueIdentifierAttributeName("nsuniqueid"); + } - return new DefaultPartitionManager(identityConfigs); + LDAPMappingConfigurationBuilder ldapUserMappingBuilder = ldapStoreBuilder + .mapping(User.class) + .baseDN(ldapConfig.get(LDAPConstants.USER_DN_SUFFIX)) + .objectClasses(userObjectClasses) + .attribute("loginName", ldapLoginNameMapping, true) + .attribute("firstName", ldapFirstNameMapping) + .attribute("lastName", SN) + .attribute("email", EMAIL); + + if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) { + ldapUserMappingBuilder.bindingAttribute("fullName", CN); + logger.infof("Using 'cn' attribute for DN of user and 'sAMAccountName' for username"); + } + + KeycloakEventBridge eventBridge = new KeycloakEventBridge(activeDirectory && "true".equals(ldapConfig.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE))); + return new DefaultPartitionManager(builder.buildAll(), eventBridge, null); } private static void checkSystemProperty(String name, String defaultValue) { @@ -120,16 +127,6 @@ public class PartitionManagerRegistry { } } - private static 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; - } - // Parse array of strings like [ "inetOrgPerson", "organizationalPerson" ] from the string like: "inetOrgPerson, organizationalPerson" private static String[] getUserObjectClasses(Map ldapConfig) { String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES); diff --git a/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory b/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory new file mode 100644 index 0000000000..5bfaf9d173 --- /dev/null +++ b/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory @@ -0,0 +1 @@ +org.keycloak.picketlink.ldap.LDAPPartitionManagerProviderFactory \ No newline at end of file diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java deleted file mode 100644 index 124bb8294e..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.keycloak.picketlink.idm; - -import java.lang.reflect.Method; - -import javax.naming.directory.BasicAttributes; - -import org.keycloak.models.utils.reflection.Reflections; -import org.picketlink.idm.config.LDAPMappingConfiguration; -import org.picketlink.idm.ldap.internal.LDAPIdentityStore; -import org.picketlink.idm.ldap.internal.LDAPOperationManager; -import org.picketlink.idm.model.AttributedType; -import org.picketlink.idm.model.basic.User; -import org.picketlink.idm.query.IdentityQuery; -import org.picketlink.idm.spi.IdentityContext; - -import static org.picketlink.common.constants.LDAPConstants.CN; -import static org.picketlink.common.constants.LDAPConstants.COMMA; -import static org.picketlink.common.constants.LDAPConstants.EQUAL; - -/** - * @author Marek Posolda - */ -public class KeycloakLDAPIdentityStore extends LDAPIdentityStore { - - public static Method GET_OPERATION_MANAGER_METHOD; - public static Method CREATE_SEARCH_FILTER_METHOD; - public static Method EXTRACT_ATTRIBUTES_METHOD; - public static Method GET_ENTRY_IDENTIFIER_METHOD; - - public static final String SAM_ACCOUNT_NAME = "sAMAccountName"; - - static { - GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager"); - CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class); - EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class); - GET_ENTRY_IDENTIFIER_METHOD = getMethodOnLDAPStore("getEntryIdentifier", AttributedType.class); - } - - @Override - public void addAttributedType(IdentityContext context, AttributedType attributedType) { - LDAPMappingConfiguration userMappingConfig = getConfig().getMappingConfig(attributedType.getClass()); - String ldapUsernameAttrName = userMappingConfig.getMappedProperties().get(userMappingConfig.getIdProperty().getName()); - - if (getConfig().isActiveDirectory() && SAM_ACCOUNT_NAME.equals(ldapUsernameAttrName)) { - // TODO: pain, but everything in picketlink is private... Improve if possible... - LDAPOperationManager operationManager = Reflections.invokeMethod(false, GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, this); - - String cn = getCommonName(attributedType); - String bindingDN = CN + EQUAL + cn + COMMA + userMappingConfig.getBaseDN(); - - BasicAttributes ldapAttributes = Reflections.invokeMethod(false, EXTRACT_ATTRIBUTES_METHOD, BasicAttributes.class, this, attributedType, true); - ldapAttributes.put(CN, cn); - - operationManager.createSubContext(bindingDN, ldapAttributes); - - String ldapId = Reflections.invokeMethod(false, GET_ENTRY_IDENTIFIER_METHOD, String.class, this, attributedType); - attributedType.setId(ldapId); - } else { - super.addAttributedType(context, attributedType); - } - } - - // Hack as methods are protected on LDAPIdentityStore :/ - public 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); - } - } - - protected String getCommonName(AttributedType aType) { - User user = (User)aType; - String fullName; - if (user.getFirstName() != null && user.getLastName() != null) { - fullName = user.getFirstName() + " " + user.getLastName(); - } else if (user.getFirstName() != null && user.getFirstName().trim().length() > 0) { - fullName = user.getFirstName(); - } else { - fullName = user.getLastName(); - } - - // Fallback to loginName - if (fullName == null || fullName.trim().length() == 0) { - fullName = user.getLoginName(); - } - - return fullName; - } -} 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 deleted file mode 100644 index cf4d70c81b..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.keycloak.picketlink.idm; - -import java.io.UnsupportedEncodingException; -import java.util.Date; -import java.util.List; - -import javax.naming.NamingException; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.DirContext; -import javax.naming.directory.ModificationItem; -import javax.naming.directory.SearchResult; - -import org.keycloak.models.utils.reflection.Reflections; -import org.picketlink.idm.IdentityManager; -import org.picketlink.idm.config.LDAPMappingConfiguration; -import org.picketlink.idm.credential.Credentials; -import org.picketlink.idm.credential.Password; -import org.picketlink.idm.credential.UsernamePasswordCredentials; -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.basic.User; -import org.picketlink.idm.query.IdentityQuery; -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 { - - // 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) { - // TODO: Don't setup it here once PLINK-508 is fixed - 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 User 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) { - if (!store.getConfig().isActiveDirectory()) { - super.update(context, account, password, store, effectiveDate, expiryDate); - } else { - User user = (User)account; - LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store); - IdentityManager identityManager = getIdentityManager(context); - String userDN = getDNOfUser(user, identityManager, store, operationManager); - - updateADPassword(userDN, new String(password.getValue()), operationManager); - - if (userAccountControlAfterPasswordUpdate != null) { - ModificationItem[] mods = new ModificationItem[1]; - BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate); - mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0); - operationManager.modifyAttribute(userDN, mod0); - } - } - } - - protected void updateADPassword(String userDN, String password, LDAPOperationManager operationManager) { - 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); - operationManager.modifyAttribute(userDN, unicodePwd); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - @Override - public void validate(IdentityContext context, UsernamePasswordCredentials credentials, - LDAPIdentityStore store) { - credentials.setStatus(Credentials.Status.INVALID); - credentials.setValidatedAccount(null); - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Validating credentials [%s][%s] using identity store [%s] and credential handler [%s].", credentials.getClass(), credentials, store, this); - } - - User account = getAccount(context, credentials.getUsername()); - - // If the user for the provided username cannot be found we fail validation - if (account != null) { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Found account [%s] from credentials [%s].", account, credentials); - } - - if (account.isEnabled()) { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account [%s] is ENABLED.", account, credentials); - } - - char[] password = credentials.getPassword().getValue(); - - // String bindingDN = store.getBindingDN(account); - - LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store); - String bindingDN = getDNOfUser(account, getIdentityManager(context), store, operationManager); - - if (operationManager.authenticate(bindingDN, new String(password))) { - credentials.setValidatedAccount(account); - credentials.setStatus(Credentials.Status.VALID); - } - } else { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account [%s] is DISABLED.", account, credentials); - } - credentials.setStatus(Credentials.Status.ACCOUNT_DISABLED); - } - } else { - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Account NOT FOUND for credentials [%s][%s].", credentials.getClass(), credentials); - } - } - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Credential [%s][%s] validated using identity store [%s] and credential handler [%s]. Status [%s]. Validated Account [%s]", - credentials.getClass(), credentials, store, this, credentials.getStatus(), credentials.getValidatedAccount()); - } - } - - // TODO: remove later... It's needed just because LDAPIdentityStore.getBindingDN, which always uses idProperty as first part of DN, but in AD it doesn't work as we may have idProperty 'sAMAccountName' - // but DN like: cn=John Doe,OU=foo,DC=bar - protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) { - - LDAPMappingConfiguration ldapEntryConfig = ldapStore.getConfig().getMappingConfig(User.class); - IdentityQuery identityQuery = identityManager.createIdentityQuery(User.class) - .setParameter(User.LOGIN_NAME, user.getLoginName()); - StringBuilder filter = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.CREATE_SEARCH_FILTER_METHOD, StringBuilder.class, ldapStore, identityQuery, ldapEntryConfig); - - List search = null; - try { - search = operationManager.search(ldapEntryConfig.getBaseDN(), filter.toString(), ldapEntryConfig); - } catch (NamingException ne) { - throw new RuntimeException(ne); - } - - if (search.size() > 0) { - SearchResult sr1 = search.get(0); - return sr1.getNameInNamespace(); - } else { - return null; - } - } -} 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 deleted file mode 100644 index a084eb4e12..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.keycloak.picketlink.realm; - -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; - -import org.jboss.logging.Logger; -import org.keycloak.models.LDAPConstants; -import org.keycloak.models.RealmModel; -import org.keycloak.picketlink.idm.KeycloakLDAPIdentityStore; -import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler; -import org.picketlink.idm.PartitionManager; -import org.picketlink.idm.config.AbstractIdentityStoreConfiguration; -import org.picketlink.idm.config.IdentityConfiguration; -import org.picketlink.idm.config.IdentityConfigurationBuilder; -import org.picketlink.idm.config.IdentityStoreConfiguration; -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.EMAIL; -import static org.picketlink.common.constants.LDAPConstants.SN; -import static org.picketlink.common.constants.LDAPConstants.UID; - -/** - * @author Marek Posolda - */ -public class PartitionManagerRegistry { - - private static final Logger logger = Logger.getLogger(PartitionManagerRegistry.class); - - private Map partitionManagers = new ConcurrentHashMap(); - - public PartitionManager getPartitionManager(RealmModel realm) { - Map ldapConfig = realm.getLdapServerConfig(); - if (ldapConfig == null || ldapConfig.isEmpty()) { - logger.warnf("Ldap configuration is missing for realm '%s'", realm.getName()); - return null; - } - - PartitionManagerContext context = partitionManagers.get(realm.getId()); - - // 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, 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); - } - return context.partitionManager; - } - - /** - * @param ldapConfig from realm - * @return PartitionManager instance based on LDAP store - */ - protected PartitionManager createPartitionManager(Map ldapConfig) { - IdentityConfigurationBuilder builder = new IdentityConfigurationBuilder(); - - Properties connectionProps = new Properties(); - connectionProps.put("com.sun.jndi.ldap.connect.pool", "true"); - - 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", "10"); - 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"); - - 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); - - String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE); - if (ldapLoginNameMapping == null) { - ldapLoginNameMapping = activeDirectory ? CN : UID; - } - - // 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? - ldapLoginNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.loginName", ldapLoginNameMapping, ldapLoginNameMapping, activeDirectory); - String ldapFirstNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.firstName", CN, "givenName", activeDirectory); - String ldapLastNameMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.lastName", SN, SN, activeDirectory); - String ldapEmailMapping = getNameOfLDAPAttribute("keycloak.ldap.idm.email", EMAIL, EMAIL, activeDirectory); - - String[] userObjectClasses = getUserObjectClasses(ldapConfig); - - logger.infof("LDAP Attributes mapping: loginName: %s, firstName: %s, lastName: %s, email: %s", ldapLoginNameMapping, ldapFirstNameMapping, ldapLastNameMapping, ldapEmailMapping); - - // Use same mapping for User and Agent for now - builder - .named("SIMPLE_LDAP_STORE_CONFIG") - .stores() - .ldap() - .connectionProperties(connectionProps) - .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(userObjectClasses) - .attribute("loginName", ldapLoginNameMapping, true) - .attribute("firstName", ldapFirstNameMapping) - .attribute("lastName", ldapLastNameMapping) - .attribute("email", ldapEmailMapping); - - // Workaround to override the LDAPIdentityStore with our own :/ - List identityConfigs = builder.buildAll(); - IdentityStoreConfiguration identityStoreConfig = identityConfigs.get(0).getStoreConfiguration().get(0); - ((AbstractIdentityStoreConfiguration)identityStoreConfig).setIdentityStoreType(KeycloakLDAPIdentityStore.class); - - return new DefaultPartitionManager(identityConfigs); - } - - private void checkSystemProperty(String name, String defaultValue) { - if (System.getProperty(name) == null) { - System.setProperty(name, defaultValue); - } - } - - 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; - } - - // Parse array of strings like [ "inetOrgPerson", "organizationalPerson" ] from the string like: "inetOrgPerson, organizationalPerson" - private String[] getUserObjectClasses(Map ldapConfig) { - String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES); - String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson, organizationalPerson"; - - String[] objectClasses = objClassesStr.split(","); - - // Trim them - String[] userObjectClasses = new String[objectClasses.length]; - for (int i=0 ; i config, PartitionManager manager) { - this.config = config; - this.partitionManager = manager; - } - - private Map config; - private PartitionManager partitionManager; - } -} 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 deleted file mode 100644 index 849aa98e3b..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/RealmIdentityManagerProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -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; - -/** - * @author Marek Posolda - */ -public class RealmIdentityManagerProvider extends AbstractIdentityManagerProvider { - - private final PartitionManagerRegistry partitionManagerRegistry; - - public RealmIdentityManagerProvider(PartitionManagerRegistry partitionManagerRegistry) { - this.partitionManagerRegistry = partitionManagerRegistry; - } - - @Override - protected PartitionManager getPartitionManager(RealmModel realm) { - return partitionManagerRegistry.getPartitionManager(realm); - } -} diff --git a/picketlink/keycloak-picketlink-realm/src/main/resources/META-INF/services/org.keycloak.picketlink.IdentityManagerProviderFactory b/picketlink/keycloak-picketlink-realm/src/main/resources/META-INF/services/org.keycloak.picketlink.IdentityManagerProviderFactory deleted file mode 100644 index 87130cbf8b..0000000000 --- a/picketlink/keycloak-picketlink-realm/src/main/resources/META-INF/services/org.keycloak.picketlink.IdentityManagerProviderFactory +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.picketlink.realm.RealmIdentityManagerProviderFactory \ No newline at end of file diff --git a/picketlink/pom.xml b/picketlink/pom.xml index 30d9b06e73..4cefc46f5e 100755 --- a/picketlink/pom.xml +++ b/picketlink/pom.xml @@ -17,6 +17,7 @@ keycloak-picketlink-api + keycloak-picketlink-ldap diff --git a/pom.xml b/pom.xml index 6e8c88ddd2..dcecacf3c4 100755 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 2.3.7.Final 3.0.8.Final 1.0.15.Final - 2.6.0.CR5 + 2.7.0.Beta1-20140731 1.0.2.Final 2.11.3 3.1.4.GA diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json index 6f7b21e420..7101a8f8fb 100755 --- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json @@ -78,7 +78,7 @@ "host": "${keycloak.connectionsMongo.host:127.0.0.1}", "port": "${keycloak.connectionsMongo.port:27017}", "db": "${keycloak.connectionsMongo.db:keycloak}", - "clearOnStartup": "${keycloak.connectionsMongo.clearOnStartup:true}" + "clearOnStartup": "${keycloak.connectionsMongo.clearOnStartup:false}" } } } \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/LDAPTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/LDAPTestUtils.java index fa9f703fe8..cf1ab6126a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/LDAPTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/LDAPTestUtils.java @@ -1,9 +1,6 @@ package org.keycloak.testsuite; -import org.keycloak.federation.ldap.PartitionManagerRegistry; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.picketlink.IdentityManagerProvider; +import org.keycloak.picketlink.ldap.PartitionManagerRegistry; import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.credential.Password;