Upgrade to latest picketlink. Fix LDAP configuration according to that

This commit is contained in:
mposolda 2014-07-18 13:54:23 +02:00
parent d96b660686
commit 46a0caf2e0
29 changed files with 233 additions and 836 deletions

View file

@ -112,6 +112,11 @@
<artifactId>keycloak-picketlink-api</artifactId> <artifactId>keycloak-picketlink-api</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-picketlink-ldap</artifactId>
<version>${project.version}</version>
</dependency>
<!-- mongo --> <!-- mongo -->
<dependency> <dependency>

View file

@ -25,6 +25,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-picketlink-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId> <artifactId>resteasy-jaxrs</artifactId>

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -16,6 +16,7 @@ import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.credential.Credentials; import org.picketlink.idm.credential.Credentials;
import org.picketlink.idm.credential.Password; import org.picketlink.idm.credential.Password;
import org.picketlink.idm.credential.UsernamePasswordCredentials; 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.BasicModel;
import org.picketlink.idm.model.basic.User; import org.picketlink.idm.model.basic.User;
import org.picketlink.idm.query.IdentityQuery; import org.picketlink.idm.query.IdentityQuery;
@ -123,6 +124,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
picketlinkUser.setFirstName(user.getFirstName()); picketlinkUser.setFirstName(user.getFirstName());
picketlinkUser.setLastName(user.getLastName()); picketlinkUser.setLastName(user.getLastName());
picketlinkUser.setEmail(user.getEmail()); picketlinkUser.setEmail(user.getEmail());
picketlinkUser.setAttribute(new Attribute("fullName", getFullName(user)));
identityManager.add(picketlinkUser); identityManager.add(picketlinkUser);
user.setAttribute(LDAP_ID, picketlinkUser.getId()); user.setAttribute(LDAP_ID, picketlinkUser.getId());
return proxy(user); return proxy(user);
@ -321,4 +323,23 @@ public class LDAPFederationProvider implements UserFederationProvider {
public void close() { public void close() {
//To change body of implemented methods use File | Settings | File Templates. //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;
}
} }

View file

@ -5,10 +5,10 @@ import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.picketlink.PartitionManagerProvider;
import org.picketlink.idm.PartitionManager; import org.picketlink.idm.PartitionManager;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -18,7 +18,6 @@ import java.util.Set;
*/ */
public class LDAPFederationProviderFactory implements UserFederationProviderFactory { public class LDAPFederationProviderFactory implements UserFederationProviderFactory {
public static final String PROVIDER_NAME = "ldap"; public static final String PROVIDER_NAME = "ldap";
PartitionManagerRegistry registry;
@Override @Override
public UserFederationProvider create(KeycloakSession session) { public UserFederationProvider create(KeycloakSession session) {
@ -27,13 +26,13 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
@Override @Override
public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { 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); return new LDAPFederationProvider(session, model, partition);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
registry = new PartitionManagerRegistry();
} }
@Override @Override

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<User> 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<SearchResult> 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;
}
}
}

View file

@ -18,4 +18,6 @@ public class LDAPConstants {
public static final String USER_DN_SUFFIX = "userDnSuffix"; public static final String USER_DN_SUFFIX = "userDnSuffix";
public static final String BIND_DN = "bindDn"; public static final String BIND_DN = "bindDn";
public static final String BIND_CREDENTIAL = "bindCredential"; public static final String BIND_CREDENTIAL = "bindCredential";
public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate";
} }

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -1,13 +0,0 @@
package org.keycloak.picketlink;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.picketlink.idm.IdentityManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface IdentityManagerProvider extends Provider {
IdentityManager getIdentityManager(RealmModel realm);
}

View file

@ -0,0 +1,14 @@
package org.keycloak.picketlink;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.provider.Provider;
import org.picketlink.idm.PartitionManager;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface PartitionManagerProvider extends Provider {
PartitionManager getPartitionManager(UserFederationProviderModel model);
}

View file

@ -5,5 +5,5 @@ import org.keycloak.provider.ProviderFactory;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public interface IdentityManagerProviderFactory extends ProviderFactory<IdentityManagerProvider> { public interface PartitionManagerProviderFactory extends ProviderFactory<PartitionManagerProvider> {
} }

View file

@ -7,19 +7,19 @@ import org.keycloak.provider.Spi;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class IdentityManagerSpi implements Spi { public class PartitionManagerSpi implements Spi {
@Override @Override
public String getName() { public String getName() {
return "picketlink-identity-manager"; return "picketlink-idm";
} }
@Override @Override
public Class<? extends Provider> getProviderClass() { public Class<? extends Provider> getProviderClass() {
return IdentityManagerProvider.class; return PartitionManagerProvider.class;
} }
@Override @Override
public Class<? extends ProviderFactory> getProviderFactoryClass() { public Class<? extends ProviderFactory> getProviderFactoryClass() {
return IdentityManagerProviderFactory.class; return PartitionManagerProviderFactory.class;
} }
} }

View file

@ -1 +1 @@
org.keycloak.picketlink.IdentityManagerSpi org.keycloak.picketlink.PartitionManagerSpi

View file

@ -10,8 +10,8 @@
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-picketlink-realm</artifactId> <artifactId>keycloak-picketlink-ldap</artifactId>
<name>Keycloak Picketlink Realm</name> <name>Keycloak Picketlink LDAP</name>
<description /> <description />
<dependencies> <dependencies>

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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");
}
}
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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() {
}
}

View file

@ -1,23 +1,23 @@
package org.keycloak.picketlink.realm; package org.keycloak.picketlink.ldap;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.picketlink.IdentityManagerProvider; import org.keycloak.picketlink.PartitionManagerProvider;
import org.keycloak.picketlink.IdentityManagerProviderFactory; import org.keycloak.picketlink.PartitionManagerProviderFactory;
import org.picketlink.idm.PartitionManager; 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class RealmIdentityManagerProviderFactory implements IdentityManagerProviderFactory { public class LDAPPartitionManagerProviderFactory implements PartitionManagerProviderFactory {
private PartitionManagerRegistry partitionManagerRegistry; private PartitionManagerRegistry partitionManagerRegistry;
@Override @Override
public IdentityManagerProvider create(KeycloakSession session) { public PartitionManagerProvider create(KeycloakSession session) {
return new RealmIdentityManagerProvider(partitionManagerRegistry); return new LDAPPartitionManagerProvider(partitionManagerRegistry);
} }
@Override @Override
@ -31,7 +31,7 @@ public class RealmIdentityManagerProviderFactory implements IdentityManagerProvi
@Override @Override
public String getId() { public String getId() {
return "realm"; return "ldap";
} }
} }

View file

@ -1,23 +1,26 @@
package org.keycloak.federation.ldap; package org.keycloak.picketlink.ldap;
import org.jboss.logging.Logger; import java.util.HashMap;
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.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap; 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -34,8 +37,8 @@ public class PartitionManagerRegistry {
// Ldap config might have changed for the realm. In this case, we must re-initialize // Ldap config might have changed for the realm. In this case, we must re-initialize
Map<String, String> config = model.getConfig(); Map<String, String> config = model.getConfig();
if (context == null || !config.equals(context.config)) { 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(), logLDAPConfig(model.getId(), config);
config.get(LDAPConstants.CONNECTION_URL), config.get(LDAPConstants.BASE_DN), config.get(LDAPConstants.VENDOR));
PartitionManager manager = createPartitionManager(config); PartitionManager manager = createPartitionManager(config);
context = new PartitionManagerContext(config, manager); context = new PartitionManagerContext(config, manager);
partitionManagers.put(model.getId(), context); partitionManagers.put(model.getId(), context);
@ -43,6 +46,13 @@ public class PartitionManagerRegistry {
return context.partitionManager; return context.partitionManager;
} }
// Don't log LDAP password
private void logLDAPConfig(String fedProviderId, Map<String, String> ldapConfig) {
Map<String, String> copy = new HashMap<String, String>(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 * @param ldapConfig from realm
* @return PartitionManager instance based on LDAP store * @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.authentication", "none simple");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1"); 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.prefsize", "5");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.timeout", "300000"); 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.protocol", "plain");
@ -63,11 +73,6 @@ public class PartitionManagerRegistry {
String vendor = ldapConfig.get(LDAPConstants.VENDOR); 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); boolean activeDirectory = vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE); String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
@ -75,17 +80,11 @@ public class PartitionManagerRegistry {
ldapLoginNameMapping = activeDirectory ? CN : UID; 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? String ldapFirstNameMapping = activeDirectory ? "givenName" : CN;
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); 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 // Use same mapping for User and Agent for now
LDAPStoreConfigurationBuilder ldapStoreBuilder =
builder builder
.named("SIMPLE_LDAP_STORE_CONFIG") .named("SIMPLE_LDAP_STORE_CONFIG")
.stores() .stores()
@ -97,21 +96,29 @@ public class PartitionManagerRegistry {
.bindCredential(ldapConfig.get(LDAPConstants.BIND_CREDENTIAL)) .bindCredential(ldapConfig.get(LDAPConstants.BIND_CREDENTIAL))
.url(ldapConfig.get(LDAPConstants.CONNECTION_URL)) .url(ldapConfig.get(LDAPConstants.CONNECTION_URL))
.activeDirectory(activeDirectory) .activeDirectory(activeDirectory)
.supportAllFeatures() .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 :/ // RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID"
List<IdentityConfiguration> identityConfigs = builder.buildAll(); if (vendor != null && vendor.equals(LDAPConstants.VENDOR_RHDS)) {
IdentityStoreConfiguration identityStoreConfig = identityConfigs.get(0).getStoreConfiguration().get(0); ldapStoreBuilder.uniqueIdentifierAttributeName("nsuniqueid");
((AbstractIdentityStoreConfiguration)identityStoreConfig).setIdentityStoreType(KeycloakLDAPIdentityStore.class); }
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) { 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" // Parse array of strings like [ "inetOrgPerson", "organizationalPerson" ] from the string like: "inetOrgPerson, organizationalPerson"
private static String[] getUserObjectClasses(Map<String,String> ldapConfig) { private static String[] getUserObjectClasses(Map<String,String> ldapConfig) {
String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES); String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES);

View file

@ -0,0 +1 @@
org.keycloak.picketlink.ldap.LDAPPartitionManagerProviderFactory

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<User> 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<SearchResult> 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;
}
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PartitionManagerRegistry {
private static final Logger logger = Logger.getLogger(PartitionManagerRegistry.class);
private Map<String, PartitionManagerContext> partitionManagers = new ConcurrentHashMap<String, PartitionManagerContext>();
public PartitionManager getPartitionManager(RealmModel realm) {
Map<String,String> 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<String,String> 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<IdentityConfiguration> 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<String,String> 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<objectClasses.length ; i++) {
userObjectClasses[i] = objectClasses[i].trim();
}
return userObjectClasses;
}
private class PartitionManagerContext {
private PartitionManagerContext(Map<String,String> config, PartitionManager manager) {
this.config = config;
this.partitionManager = manager;
}
private Map<String,String> config;
private PartitionManager partitionManager;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);
}
}

View file

@ -1 +0,0 @@
org.keycloak.picketlink.realm.RealmIdentityManagerProviderFactory

View file

@ -17,6 +17,7 @@
<modules> <modules>
<module>keycloak-picketlink-api</module> <module>keycloak-picketlink-api</module>
<module>keycloak-picketlink-ldap</module>
</modules> </modules>

View file

@ -18,7 +18,7 @@
<resteasy.version>2.3.7.Final</resteasy.version> <resteasy.version>2.3.7.Final</resteasy.version>
<resteasy.version.latest>3.0.8.Final</resteasy.version.latest> <resteasy.version.latest>3.0.8.Final</resteasy.version.latest>
<undertow.version>1.0.15.Final</undertow.version> <undertow.version>1.0.15.Final</undertow.version>
<picketlink.version>2.6.0.CR5</picketlink.version> <picketlink.version>2.7.0.Beta1-20140731</picketlink.version>
<picketbox.ldap.version>1.0.2.Final</picketbox.ldap.version> <picketbox.ldap.version>1.0.2.Final</picketbox.ldap.version>
<mongo.driver.version>2.11.3</mongo.driver.version> <mongo.driver.version>2.11.3</mongo.driver.version>
<jboss.logging.version>3.1.4.GA</jboss.logging.version> <jboss.logging.version>3.1.4.GA</jboss.logging.version>

View file

@ -78,7 +78,7 @@
"host": "${keycloak.connectionsMongo.host:127.0.0.1}", "host": "${keycloak.connectionsMongo.host:127.0.0.1}",
"port": "${keycloak.connectionsMongo.port:27017}", "port": "${keycloak.connectionsMongo.port:27017}",
"db": "${keycloak.connectionsMongo.db:keycloak}", "db": "${keycloak.connectionsMongo.db:keycloak}",
"clearOnStartup": "${keycloak.connectionsMongo.clearOnStartup:true}" "clearOnStartup": "${keycloak.connectionsMongo.clearOnStartup:false}"
} }
} }
} }

View file

@ -1,9 +1,6 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.keycloak.federation.ldap.PartitionManagerRegistry; import org.keycloak.picketlink.ldap.PartitionManagerRegistry;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.picketlink.IdentityManagerProvider;
import org.picketlink.idm.IdentityManager; import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager; import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.credential.Password; import org.picketlink.idm.credential.Password;