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>
|
<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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
* @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>
|
* @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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +1 @@
|
||||||
org.keycloak.picketlink.IdentityManagerSpi
|
org.keycloak.picketlink.PartitionManagerSpi
|
|
@ -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>
|
|
@ -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.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";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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);
|
|
@ -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>
|
<modules>
|
||||||
<module>keycloak-picketlink-api</module>
|
<module>keycloak-picketlink-api</module>
|
||||||
|
<module>keycloak-picketlink-ldap</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue