Upgrade to latest picketlink. Fix LDAP configuration according to that
This commit is contained in:
parent
d96b660686
commit
46a0caf2e0
29 changed files with 233 additions and 836 deletions
5
dependencies/server-all/pom.xml
vendored
5
dependencies/server-all/pom.xml
vendored
|
@ -112,6 +112,11 @@
|
|||
<artifactId>keycloak-picketlink-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-picketlink-ldap</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- mongo -->
|
||||
<dependency>
|
||||
|
|
|
@ -25,6 +25,12 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-picketlink-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -5,5 +5,5 @@ import org.keycloak.provider.ProviderFactory;
|
|||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface IdentityManagerProviderFactory extends ProviderFactory<IdentityManagerProvider> {
|
||||
public interface PartitionManagerProviderFactory extends ProviderFactory<PartitionManagerProvider> {
|
||||
}
|
|
@ -7,19 +7,19 @@ import org.keycloak.provider.Spi;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class IdentityManagerSpi implements Spi {
|
||||
public class PartitionManagerSpi implements Spi {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "picketlink-identity-manager";
|
||||
return "picketlink-idm";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return IdentityManagerProvider.class;
|
||||
return PartitionManagerProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return IdentityManagerProviderFactory.class;
|
||||
return PartitionManagerProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
org.keycloak.picketlink.IdentityManagerSpi
|
||||
org.keycloak.picketlink.PartitionManagerSpi
|
|
@ -10,8 +10,8 @@
|
|||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-picketlink-realm</artifactId>
|
||||
<name>Keycloak Picketlink Realm</name>
|
||||
<artifactId>keycloak-picketlink-ldap</artifactId>
|
||||
<name>Keycloak Picketlink LDAP</name>
|
||||
<description />
|
||||
|
||||
<dependencies>
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <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
|
||||
Map<String, String> 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<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
|
||||
* @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()
|
||||
.supportAllFeatures();
|
||||
|
||||
// RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID"
|
||||
if (vendor != null && vendor.equals(LDAPConstants.VENDOR_RHDS)) {
|
||||
ldapStoreBuilder.uniqueIdentifierAttributeName("nsuniqueid");
|
||||
}
|
||||
|
||||
LDAPMappingConfigurationBuilder ldapUserMappingBuilder = ldapStoreBuilder
|
||||
.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);
|
||||
.attribute("lastName", SN)
|
||||
.attribute("email", EMAIL);
|
||||
|
||||
// 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);
|
||||
if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) {
|
||||
ldapUserMappingBuilder.bindingAttribute("fullName", CN);
|
||||
logger.infof("Using 'cn' attribute for DN of user and 'sAMAccountName' for username");
|
||||
}
|
||||
|
||||
return new DefaultPartitionManager(identityConfigs);
|
||||
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<String,String> ldapConfig) {
|
||||
String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES);
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.picketlink.ldap.LDAPPartitionManagerProviderFactory
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
org.keycloak.picketlink.realm.RealmIdentityManagerProviderFactory
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<modules>
|
||||
<module>keycloak-picketlink-api</module>
|
||||
<module>keycloak-picketlink-ldap</module>
|
||||
</modules>
|
||||
|
||||
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -18,7 +18,7 @@
|
|||
<resteasy.version>2.3.7.Final</resteasy.version>
|
||||
<resteasy.version.latest>3.0.8.Final</resteasy.version.latest>
|
||||
<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>
|
||||
<mongo.driver.version>2.11.3</mongo.driver.version>
|
||||
<jboss.logging.version>3.1.4.GA</jboss.logging.version>
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue