Merge pull request #1126 from mposolda/master

KEYCLOAK-1007 Fork Picketlink LDAP code. Remove picketlink dependencies ...
This commit is contained in:
Marek Posolda 2015-04-08 22:52:57 +02:00
commit b92a178142
61 changed files with 4042 additions and 1013 deletions

View file

@ -144,34 +144,6 @@
<artifactId>keycloak-kerberos-federation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-common</artifactId>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-api</artifactId>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-impl</artifactId>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-simple-schema</artifactId>
</dependency>
<!-- picketlink -->
<dependency>
<groupId>org.keycloak</groupId>
<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>
<!-- saml -->
<dependency>

View file

@ -259,14 +259,6 @@
<maven-resource group="org.keycloak" artifact="keycloak-ldap-federation"/>
</module-def>
<module-def name="org.keycloak.keycloak-picketlink-api">
<maven-resource group="org.keycloak" artifact="keycloak-picketlink-api"/>
</module-def>
<module-def name="org.keycloak.keycloak-picketlink-ldap">
<maven-resource group="org.keycloak" artifact="keycloak-picketlink-ldap"/>
</module-def>
<module-def name="org.keycloak.keycloak-saml-core">
<maven-resource group="org.keycloak" artifact="keycloak-saml-core"/>
</module-def>

View file

@ -10,14 +10,9 @@
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-kerberos-federation"/>
<module name="org.keycloak.keycloak-picketlink-api"/>
<module name="javax.ws.rs.api"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
<module name="org.jboss.logging"/>
<module name="org.picketlink.common"/>
<module name="org.picketlink.idm.api"/>
<module name="org.picketlink.idm"/>
<module name="org.picketlink.idm.schema"/>
<module name="javax.api"/>
</dependencies>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-picketlink-api">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-picketlink-api"/>
<module name="org.jboss.logging"/>
<module name="org.picketlink.common"/>
<module name="org.picketlink.idm.api"/>
<module name="org.picketlink.idm"/>
<module name="org.picketlink.idm.schema"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-picketlink-ldap">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-picketlink-api"/>
<module name="org.jboss.logging"/>
<module name="org.picketlink.common"/>
<module name="org.picketlink.idm.api"/>
<module name="org.picketlink.idm"/>
<module name="org.picketlink.idm.schema"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -48,8 +48,6 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
<module name="org.keycloak.keycloak-picketlink-api" services="import"/>
<module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>

View file

@ -49,10 +49,8 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
<module name="org.keycloak.keycloak-picketlink-api" services="import"/>
<module name="org.keycloak.keycloak-wildfly-extensions" services="import"/>
<module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>

View file

@ -40,9 +40,6 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
<module name="org.keycloak.keycloak-picketlink-api" services="import"/>
<module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>

View file

@ -31,12 +31,6 @@
<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>
@ -61,27 +55,6 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-impl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-simple-schema</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>

View file

@ -3,6 +3,10 @@ package org.keycloak.federation.ldap;
import org.jboss.logging.Logger;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
@ -16,12 +20,6 @@ import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.constants.KerberosConstants;
import org.picketlink.idm.IdentityManagementException;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.basic.BasicModel;
import org.picketlink.idm.model.basic.User;
import org.picketlink.idm.query.IdentityQuery;
import java.util.Arrays;
import java.util.HashMap;
@ -38,23 +36,21 @@ import java.util.Set;
*/
public class LDAPFederationProvider implements UserFederationProvider {
private static final Logger logger = Logger.getLogger(LDAPFederationProvider.class);
public static final String LDAP_ID = "LDAP_ID";
public static final String SYNC_REGISTRATIONS = "syncRegistrations";
protected LDAPFederationProviderFactory factory;
protected KeycloakSession session;
protected UserFederationProviderModel model;
protected PartitionManager partitionManager;
protected LDAPIdentityStore ldapIdentityStore;
protected EditMode editMode;
protected LDAPProviderKerberosConfig kerberosConfig;
protected final Set<String> supportedCredentialTypes = new HashSet<String>();
public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, PartitionManager partitionManager) {
public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, LDAPIdentityStore ldapIdentityStore) {
this.factory = factory;
this.session = session;
this.model = model;
this.partitionManager = partitionManager;
this.ldapIdentityStore = ldapIdentityStore;
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
String editModeString = model.getConfig().get(LDAPConstants.EDIT_MODE);
if (editModeString == null) {
@ -69,16 +65,6 @@ public class LDAPFederationProvider implements UserFederationProvider {
}
}
private ModelException convertIDMException(IdentityManagementException ie) {
Throwable realCause = ie;
while (realCause.getCause() != null) {
realCause = realCause.getCause();
}
// Use the message from the realCause
return new ModelException(realCause.getMessage(), ie);
}
public KeycloakSession getSession() {
return session;
}
@ -87,8 +73,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
return model;
}
public PartitionManager getPartitionManager() {
return partitionManager;
public LDAPIdentityStore getLdapIdentityStore() {
return this.ldapIdentityStore;
}
@Override
@ -125,22 +111,18 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public boolean synchronizeRegistrations() {
return "true".equalsIgnoreCase(model.getConfig().get(SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE;
return "true".equalsIgnoreCase(model.getConfig().get(LDAPConstants.SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE;
}
@Override
public UserModel register(RealmModel realm, UserModel user) {
if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");;
if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");
if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
try {
User picketlinkUser = LDAPUtils.addUser(this.partitionManager, user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmail());
user.setAttribute(LDAP_ID, picketlinkUser.getId());
LDAPUser ldapUser = LDAPUtils.addUser(this.ldapIdentityStore, user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmail());
user.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getId());
user.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getEntryDN());
return proxy(user);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
}
@Override
@ -150,58 +132,53 @@ public class LDAPFederationProvider implements UserFederationProvider {
return false;
}
try {
return LDAPUtils.removeUser(partitionManager, user.getUsername());
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return LDAPUtils.removeUser(this.ldapIdentityStore, user.getUsername());
}
@Override
public List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults) {
List<UserModel> searchResults =new LinkedList<UserModel>();
try {
Map<String, User> plUsers = searchPicketlink(attributes, maxResults);
for (User user : plUsers.values()) {
if (session.userStorage().getUserByUsername(user.getLoginName(), realm) == null) {
UserModel imported = importUserFromPicketlink(realm, user);
Map<String, LDAPUser> ldapUsers = searchLDAP(attributes, maxResults);
for (LDAPUser ldapUser : ldapUsers.values()) {
if (session.userStorage().getUserByUsername(ldapUser.getLoginName(), realm) == null) {
UserModel imported = importUserFromLDAP(realm, ldapUser);
searchResults.add(imported);
}
}
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return searchResults;
}
protected Map<String, User> searchPicketlink(Map<String, String> attributes, int maxResults) {
IdentityManager identityManager = getIdentityManager();
Map<String, User> results = new HashMap<String, User>();
protected Map<String, LDAPUser> searchLDAP(Map<String, String> attributes, int maxResults) {
Map<String, LDAPUser> results = new HashMap<String, LDAPUser>();
if (attributes.containsKey(USERNAME)) {
User user = BasicModel.getUser(identityManager, attributes.get(USERNAME));
LDAPUser user = LDAPUtils.getUser(this.ldapIdentityStore, attributes.get(USERNAME));
if (user != null) {
results.put(user.getLoginName(), user);
}
}
if (attributes.containsKey(EMAIL)) {
User user = queryByEmail(identityManager, attributes.get(EMAIL));
LDAPUser user = queryByEmail(attributes.get(EMAIL));
if (user != null) {
results.put(user.getLoginName(), user);
}
}
if (attributes.containsKey(FIRST_NAME) || attributes.containsKey(LAST_NAME)) {
IdentityQuery<User> query = identityManager.createIdentityQuery(User.class);
IdentityQueryBuilder queryBuilder = this.ldapIdentityStore.createQueryBuilder();
IdentityQuery<LDAPUser> query = queryBuilder.createIdentityQuery(LDAPUser.class);
if (attributes.containsKey(FIRST_NAME)) {
query.setParameter(User.FIRST_NAME, attributes.get(FIRST_NAME));
query.where(queryBuilder.equal(LDAPUser.FIRST_NAME, attributes.get(FIRST_NAME)));
}
if (attributes.containsKey(LAST_NAME)) {
query.setParameter(User.LAST_NAME, attributes.get(LAST_NAME));
query.where(queryBuilder.equal(LDAPUser.LAST_NAME, attributes.get(LAST_NAME)));
}
query.setLimit(maxResults);
List<User> agents = query.getResultList();
for (User user : agents) {
List<LDAPUser> users = query.getResultList();
for (LDAPUser user : users) {
results.put(user.getLoginName(), user);
}
}
@ -211,85 +188,69 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public boolean isValid(UserModel local) {
try {
User picketlinkUser = LDAPUtils.getUser(partitionManager, local.getUsername());
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(this.ldapIdentityStore, local.getUsername());
if (ldapUser == null) {
return false;
}
return picketlinkUser.getId().equals(local.getAttribute(LDAP_ID));
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return ldapUser.getId().equals(local.getAttribute(LDAPConstants.LDAP_ID));
}
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
try {
User picketlinkUser = LDAPUtils.getUser(partitionManager, username);
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(this.ldapIdentityStore, username);
if (ldapUser == null) {
return null;
}
// KEYCLOAK-808: Should we allow case-sensitivity to be configurable?
if (!username.equals(picketlinkUser.getLoginName())) {
logger.warnf("User found in LDAP but with different username. LDAP username: %s, Searched username: %s", username, picketlinkUser.getLoginName());
if (!username.equals(ldapUser.getLoginName())) {
logger.warnf("User found in LDAP but with different username. LDAP username: %s, Searched username: %s", username, ldapUser.getLoginName());
return null;
}
return importUserFromPicketlink(realm, picketlinkUser);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return importUserFromLDAP(realm, ldapUser);
}
public IdentityManager getIdentityManager() {
return partitionManager.createIdentityManager();
protected UserModel importUserFromLDAP(RealmModel realm, LDAPUser ldapUser) {
String email = (ldapUser.getEmail() != null && ldapUser.getEmail().trim().length() > 0) ? ldapUser.getEmail() : null;
if (ldapUser.getLoginName() == null) {
throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. ID of user from LDAP: " + ldapUser.getId());
}
protected UserModel importUserFromPicketlink(RealmModel realm, User picketlinkUser) {
String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
if (picketlinkUser.getLoginName() == null) {
throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. ID of user from LDAP: " + picketlinkUser.getId());
}
UserModel imported = session.userStorage().addUser(realm, picketlinkUser.getLoginName());
UserModel imported = session.userStorage().addUser(realm, ldapUser.getLoginName());
imported.setEnabled(true);
imported.setEmail(email);
imported.setFirstName(picketlinkUser.getFirstName());
imported.setLastName(picketlinkUser.getLastName());
imported.setFirstName(ldapUser.getFirstName());
imported.setLastName(ldapUser.getLastName());
imported.setFederationLink(model.getId());
imported.setAttribute(LDAP_ID, picketlinkUser.getId());
imported.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getId());
imported.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getEntryDN());
logger.debugf("Added new user from LDAP. Username: " + imported.getUsername() + ", Email: ", imported.getEmail() + ", LDAP_ID: " + picketlinkUser.getId());
logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
ldapUser.getId(), ldapUser.getEntryDN());
return proxy(imported);
}
protected User queryByEmail(IdentityManager identityManager, String email) throws IdentityManagementException {
return LDAPUtils.getUserByEmail(identityManager, email);
protected LDAPUser queryByEmail(String email) {
return LDAPUtils.getUserByEmail(this.ldapIdentityStore, email);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
IdentityManager identityManager = getIdentityManager();
try {
User picketlinkUser = queryByEmail(identityManager, email);
if (picketlinkUser == null) {
LDAPUser ldapUser = queryByEmail(email);
if (ldapUser == null) {
return null;
}
// KEYCLOAK-808: Should we allow case-sensitivity to be configurable?
if (!email.equals(picketlinkUser.getEmail())) {
logger.warnf("User found in LDAP but with different email. LDAP email: %s, Searched email: %s", email, picketlinkUser.getEmail());
if (!email.equals(ldapUser.getEmail())) {
logger.warnf("User found in LDAP but with different email. LDAP email: %s, Searched email: %s", email, ldapUser.getEmail());
return null;
}
return importUserFromPicketlink(realm, picketlinkUser);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return importUserFromLDAP(realm, ldapUser);
}
@Override
@ -302,18 +263,14 @@ public class LDAPFederationProvider implements UserFederationProvider {
// complete I don't think we have to do anything here
}
public boolean validPassword(String username, String password) {
public boolean validPassword(UserModel user, String password) {
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) {
// Use Kerberos JAAS (Krb5LoginModule)
KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig);
return authenticator.validUser(username, password);
return authenticator.validUser(user.getUsername(), password);
} else {
// Use Naming LDAP API
try {
return LDAPUtils.validatePassword(partitionManager, username, password);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
return LDAPUtils.validatePassword(this.ldapIdentityStore, user, password);
}
}
@ -322,7 +279,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
public boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input) {
for (UserCredentialModel cred : input) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
return validPassword(user.getUsername(), cred.getValue());
return validPassword(user, cred.getValue());
} else {
return false; // invalid cred type
}
@ -353,7 +310,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
UserModel user = findOrCreateAuthenticatedUser(realm, username);
if (user == null) {
logger.warn("Kerberos/SPNEGO authentication succeeded with username [" + username + "], but couldn't find or create user with federation provider [" + model.getDisplayName() + "]");
logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getDisplayName());
return CredentialValidationOutput.failed();
} else {
String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
@ -375,24 +332,23 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public void close() {
//To change body of implemented methods use File | Settings | File Templates.
}
protected void importPicketlinkUsers(RealmModel realm, List<User> users, UserFederationProviderModel fedModel) {
for (User picketlinkUser : users) {
String username = picketlinkUser.getLoginName();
protected void importLDAPUsers(RealmModel realm, List<LDAPUser> ldapUsers, UserFederationProviderModel fedModel) {
for (LDAPUser ldapUser : ldapUsers) {
String username = ldapUser.getLoginName();
UserModel currentUser = session.userStorage().getUserByUsername(username, realm);
if (currentUser == null) {
// Add new user to Keycloak
importUserFromPicketlink(realm, picketlinkUser);
importUserFromLDAP(realm, ldapUser);
} else {
if ((fedModel.getId().equals(currentUser.getFederationLink())) && (picketlinkUser.getId().equals(currentUser.getAttribute(LDAPFederationProvider.LDAP_ID)))) {
if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getId().equals(currentUser.getAttribute(LDAPConstants.LDAP_ID)))) {
// Update keycloak user
String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
String email = (ldapUser.getEmail() != null && ldapUser.getEmail().trim().length() > 0) ? ldapUser.getEmail() : null;
currentUser.setEmail(email);
currentUser.setFirstName(picketlinkUser.getFirstName());
currentUser.setLastName(picketlinkUser.getLastName());
currentUser.setFirstName(ldapUser.getFirstName());
currentUser.setLastName(ldapUser.getLastName());
logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
} else {
logger.warnf("User '%s' is not updated during sync as he is not linked to federation provider '%s'", username, fedModel.getDisplayName());
@ -404,29 +360,29 @@ public class LDAPFederationProvider implements UserFederationProvider {
/**
* Called after successful kerberos authentication
*
* @param realm
* @param realm realm
* @param username username without realm prefix
* @return
* @return finded or newly created user
*/
protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) {
UserModel user = session.userStorage().getUserByUsername(username, realm);
if (user != null) {
logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage");
logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", username);
if (!model.getId().equals(user.getFederationLink())) {
logger.warn("User with username " + username + " already exists, but is not linked to provider [" + model.getDisplayName() + "]");
logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]", username, model.getDisplayName());
return null;
} else if (isValid(user)) {
return proxy(user);
} else {
logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getDisplayName() +
"] but is not valid. Stale LDAP_ID on local user is: " + user.getAttribute(LDAP_ID));
logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
username, model.getDisplayName(), user.getAttribute(LDAPConstants.LDAP_ID));
logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user);
}
}
// Creating user to local storage
logger.debug("Kerberos authenticated user " + username + " not in Keycloak storage. Creating him");
logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", username);
return getUserByUsername(realm, username);
}
}

View file

@ -3,10 +3,15 @@ package org.keycloak.federation.ldap;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.KerberosConfig;
import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -16,16 +21,6 @@ import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.picketlink.PartitionManagerProvider;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.IdentityType;
import org.picketlink.idm.model.basic.User;
import org.picketlink.idm.query.AttributeParameter;
import org.picketlink.idm.query.Condition;
import org.picketlink.idm.query.IdentityQuery;
import org.picketlink.idm.query.IdentityQueryBuilder;
import org.picketlink.idm.query.QueryParameter;
import java.util.Collections;
import java.util.Date;
@ -41,6 +36,8 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
private static final Logger logger = Logger.getLogger(LDAPFederationProviderFactory.class);
public static final String PROVIDER_NAME = "ldap";
private LDAPIdentityStoreRegistry ldapStoreRegistry;
@Override
public UserFederationProvider create(KeycloakSession session) {
throw new IllegalAccessError("Illegal to call this method");
@ -48,13 +45,13 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
@Override
public LDAPFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
PartitionManagerProvider idmProvider = session.getProvider(PartitionManagerProvider.class);
PartitionManager partition = idmProvider.getPartitionManager(model);
return new LDAPFederationProvider(this, session, model, partition);
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
return new LDAPFederationProvider(this, session, model, ldapIdentityStore);
}
@Override
public void init(Config.Scope config) {
this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
}
@Override
@ -64,7 +61,7 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
@Override
public void close() {
this.ldapStoreRegistry = null;
}
@Override
@ -81,9 +78,8 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) {
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date(), realmId, model.getDisplayName());
PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
IdentityQuery<User> userQuery = partitionMgr.createIdentityManager().createIdentityQuery(User.class);
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
IdentityQuery<LDAPUser> userQuery = ldapIdentityStore.createQueryBuilder().createIdentityQuery(LDAPUser.class);
syncImpl(sessionFactory, userQuery, realmId, model);
// TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
@ -91,26 +87,23 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
@Override
public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date() + ", last sync time: " + lastSync, realmId, model.getDisplayName());
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: %s, last sync time: " + lastSync, realmId, model.getDisplayName(), new Date().toString());
PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
// Sync newly created users
IdentityManager identityManager = partitionMgr.createIdentityManager();
IdentityQueryBuilder queryBuilder = identityManager.getQueryBuilder();
IdentityQueryBuilder queryBuilder = ldapIdentityStore.createQueryBuilder();
Condition condition = queryBuilder.greaterThanOrEqualTo(IdentityType.CREATED_DATE, lastSync);
IdentityQuery<User> userQuery = queryBuilder.createIdentityQuery(User.class).where(condition);
IdentityQuery<LDAPUser> userQuery = queryBuilder.createIdentityQuery(LDAPUser.class).where(condition);
syncImpl(sessionFactory, userQuery, realmId, model);
// Sync updated users
queryBuilder = identityManager.getQueryBuilder();
condition = queryBuilder.greaterThanOrEqualTo(LDAPUtils.MODIFY_DATE, lastSync);
userQuery = queryBuilder.createIdentityQuery(User.class).where(condition);
userQuery = queryBuilder.createIdentityQuery(LDAPUser.class).where(condition);
syncImpl(sessionFactory, userQuery, realmId, model);
}
protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<User> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<LDAPUser> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
boolean pagination = Boolean.parseBoolean(fedModel.getConfig().get(LDAPConstants.PAGINATION));
if (pagination) {
@ -119,36 +112,36 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
boolean nextPage = true;
while (nextPage) {
userQuery.setLimit(pageSize);
final List<User> users = userQuery.getResultList();
final List<LDAPUser> users = userQuery.getResultList();
nextPage = userQuery.getPaginationContext() != null;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
importPicketlinkUsers(session, realmId, fedModel, users);
importLdapUsers(session, realmId, fedModel, users);
}
});
}
} else {
// LDAP pagination not available. Do everything in single transaction
final List<User> users = userQuery.getResultList();
final List<LDAPUser> users = userQuery.getResultList();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
importPicketlinkUsers(session, realmId, fedModel, users);
importLdapUsers(session, realmId, fedModel, users);
}
});
}
}
protected void importPicketlinkUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<User> users) {
protected void importLdapUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<LDAPUser> ldapUsers) {
RealmModel realm = session.realms().getRealm(realmId);
LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
ldapFedProvider.importPicketlinkUsers(realm, users, fedModel);
ldapFedProvider.importLDAPUsers(realm, ldapUsers, fedModel);
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {

View file

@ -0,0 +1,165 @@
package org.keycloak.federation.ldap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStoreConfiguration;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPMappingConfiguration;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.UserFederationProviderModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPIdentityStoreRegistry {
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
public LDAPIdentityStore getLdapStore(UserFederationProviderModel model) {
LDAPIdentityStoreContext context = ldapStores.get(model.getId());
// 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)) {
logLDAPConfig(model.getId(), config);
LDAPIdentityStore store = createLdapIdentityStore(config);
context = new LDAPIdentityStoreContext(config, store);
ldapStores.put(model.getId(), context);
}
return context.store;
}
// 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
*/
public static LDAPIdentityStore createLdapIdentityStore(Map<String,String> ldapConfig) {
Properties connectionProps = new Properties();
if (ldapConfig.containsKey(LDAPConstants.CONNECTION_POOLING)) {
connectionProps.put("com.sun.jndi.ldap.connect.pool", ldapConfig.get(LDAPConstants.CONNECTION_POOLING));
}
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", "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");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
String vendor = ldapConfig.get(LDAPConstants.VENDOR);
boolean activeDirectory = vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
if (ldapLoginNameMapping == null) {
ldapLoginNameMapping = activeDirectory ? LDAPConstants.CN : LDAPConstants.UID;
}
String ldapFirstNameMapping = activeDirectory ? "givenName" : LDAPConstants.CN;
String createTimestampMapping = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
String modifyTimestampMapping = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
String[] userObjectClasses = getUserObjectClasses(ldapConfig);
boolean pagination = ldapConfig.containsKey(LDAPConstants.PAGINATION) ? Boolean.parseBoolean(ldapConfig.get(LDAPConstants.PAGINATION)) : false;
boolean userAccountControlsAfterPasswordUpdate = ldapConfig.containsKey(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE) ?
Boolean.parseBoolean(ldapConfig.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE)) : false;
// Differences of unique attribute among various vendors
String uniqueIdentifierAttributeName = LDAPConstants.ENTRY_UUID;
if (vendor != null) {
switch (vendor) {
case LDAPConstants.VENDOR_RHDS:
uniqueIdentifierAttributeName = "nsuniqueid";
break;
case LDAPConstants.VENDOR_TIVOLI:
uniqueIdentifierAttributeName = "uniqueidentifier";
break;
case LDAPConstants.VENDOR_ACTIVE_DIRECTORY:
uniqueIdentifierAttributeName = LDAPConstants.OBJECT_GUID;
}
}
LDAPIdentityStoreConfiguration ldapStoreConfig = new LDAPIdentityStoreConfiguration()
.setConnectionProperties(connectionProps)
.setBaseDN(ldapConfig.get(LDAPConstants.BASE_DN))
.setBindDN(ldapConfig.get(LDAPConstants.BIND_DN))
.setBindCredential(ldapConfig.get(LDAPConstants.BIND_CREDENTIAL))
.setLdapURL(ldapConfig.get(LDAPConstants.CONNECTION_URL))
.setActiveDirectory(activeDirectory)
.setPagination(pagination)
.setUniqueIdentifierAttributeName(uniqueIdentifierAttributeName)
.setFactoryName("com.sun.jndi.ldap.LdapCtxFactory")
.setAuthType("simple")
.setUserAccountControlsAfterPasswordUpdate(userAccountControlsAfterPasswordUpdate);
LDAPMappingConfiguration ldapUserMappingConfig = ldapStoreConfig
.mappingConfig(LDAPUser.class)
.setBaseDN(ldapConfig.get(LDAPConstants.USER_DN_SUFFIX))
.setObjectClasses(new HashSet<String>(Arrays.asList(userObjectClasses)))
.setIdPropertyName("loginName")
.addAttributeMapping("loginName", ldapLoginNameMapping)
.addAttributeMapping("firstName", ldapFirstNameMapping)
.addAttributeMapping("lastName", LDAPConstants.SN)
.addAttributeMapping("email", LDAPConstants.EMAIL)
.addReadOnlyAttributeMapping("createdDate", createTimestampMapping)
.addReadOnlyAttributeMapping("modifyDate", modifyTimestampMapping);
if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) {
ldapUserMappingConfig.setBindingPropertyName("fullName");
ldapUserMappingConfig.addAttributeMapping("fullName", LDAPConstants.CN);
logger.infof("Using 'cn' attribute for DN of user and 'sAMAccountName' for username");
}
return new LDAPIdentityStore(ldapStoreConfig);
}
private static void checkSystemProperty(String name, String defaultValue) {
if (System.getProperty(name) == null) {
System.setProperty(name, defaultValue);
}
}
// 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);
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 LDAPIdentityStoreContext {
private LDAPIdentityStoreContext(Map<String,String> config, LDAPIdentityStore store) {
this.config = config;
this.store = store;
}
private Map<String,String> config;
private LDAPIdentityStore store;
}
}

View file

@ -1,22 +1,21 @@
package org.keycloak.federation.ldap;
import org.keycloak.federation.ldap.idm.model.Attribute;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.picketlink.idm.IdentityManagementException;
import org.picketlink.idm.IdentityManager;
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.AttributeParameter;
import org.picketlink.idm.query.QueryParameter;
import org.keycloak.models.UserModel;
import java.util.List;
/**
* Allow to directly call some operations against Picketlink IDM PartitionManager (hence LDAP).
* Allow to directly call some operations against LDAPIdentityStore.
* TODO: Is this class still needed?
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -24,99 +23,102 @@ public class LDAPUtils {
public static QueryParameter MODIFY_DATE = new AttributeParameter("modifyDate");
public static User addUser(PartitionManager partitionManager, String username, String firstName, String lastName, String email) {
IdentityManager identityManager = getIdentityManager(partitionManager);
if (BasicModel.getUser(identityManager, username) != null) {
public static LDAPUser addUser(LDAPIdentityStore ldapIdentityStore, String username, String firstName, String lastName, String email) {
if (getUser(ldapIdentityStore, username) != null) {
throw new ModelDuplicateException("User with same username already exists");
}
if (getUserByEmail(identityManager, email) != null) {
if (getUserByEmail(ldapIdentityStore, email) != null) {
throw new ModelDuplicateException("User with same email already exists");
}
User picketlinkUser = new User(username);
picketlinkUser.setFirstName(firstName);
picketlinkUser.setLastName(lastName);
picketlinkUser.setEmail(email);
picketlinkUser.setAttribute(new Attribute("fullName", getFullName(username, firstName, lastName)));
identityManager.add(picketlinkUser);
return picketlinkUser;
LDAPUser ldapUser = new LDAPUser(username);
ldapUser.setFirstName(firstName);
ldapUser.setLastName(lastName);
ldapUser.setEmail(email);
ldapUser.setAttribute(new Attribute<String>("fullName", getFullName(username, firstName, lastName)));
ldapIdentityStore.add(ldapUser);
return ldapUser;
}
public static User updateUser(PartitionManager partitionManager, String username, String firstName, String lastName, String email) {
IdentityManager idmManager = getIdentityManager(partitionManager);
User picketlinkUser = BasicModel.getUser(idmManager, username);
picketlinkUser.setFirstName(firstName);
picketlinkUser.setLastName(lastName);
picketlinkUser.setEmail(email);
idmManager.update(picketlinkUser);
return picketlinkUser;
public static LDAPUser updateUser(LDAPIdentityStore ldapIdentityStore, String username, String firstName, String lastName, String email) {
LDAPUser ldapUser = getUser(ldapIdentityStore, username);
ldapUser.setFirstName(firstName);
ldapUser.setLastName(lastName);
ldapUser.setEmail(email);
ldapIdentityStore.update(ldapUser);
return ldapUser;
}
public static void updatePassword(PartitionManager partitionManager, User picketlinkUser, String password) {
IdentityManager idmManager = getIdentityManager(partitionManager);
idmManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
public static void updatePassword(LDAPIdentityStore ldapIdentityStore, UserModel user, String password) {
LDAPUser ldapUser = convertUserForPasswordUpdate(user);
ldapIdentityStore.updatePassword(ldapUser, password);
}
public static boolean validatePassword(PartitionManager partitionManager, String username, String password) {
IdentityManager idmManager = getIdentityManager(partitionManager);
UsernamePasswordCredentials credential = new UsernamePasswordCredentials();
credential.setUsername(username);
credential.setPassword(new Password(password.toCharArray()));
idmManager.validateCredentials(credential);
if (credential.getStatus() == Credentials.Status.VALID) {
return true;
} else {
return false;
}
public static void updatePassword(LDAPIdentityStore ldapIdentityStore, LDAPUser user, String password) {
ldapIdentityStore.updatePassword(user, password);
}
public static User getUser(PartitionManager partitionManager, String username) {
IdentityManager idmManager = getIdentityManager(partitionManager);
return BasicModel.getUser(idmManager, username);
public static boolean validatePassword(LDAPIdentityStore ldapIdentityStore, UserModel user, String password) {
LDAPUser ldapUser = convertUserForPasswordUpdate(user);
return ldapIdentityStore.validatePassword(ldapUser, password);
}
public static boolean validatePassword(LDAPIdentityStore ldapIdentityStore, LDAPUser user, String password) {
return ldapIdentityStore.validatePassword(user, password);
}
public static LDAPUser getUser(LDAPIdentityStore ldapIdentityStore, String username) {
return ldapIdentityStore.getUser(username);
}
// Put just username and entryDN as these are needed by LDAPIdentityStore for passwordUpdate
private static LDAPUser convertUserForPasswordUpdate(UserModel kcUser) {
LDAPUser ldapUser = new LDAPUser(kcUser.getUsername());
String ldapEntryDN = kcUser.getAttribute(LDAPConstants.LDAP_ENTRY_DN);
if (ldapEntryDN != null) {
ldapUser.setEntryDN(ldapEntryDN);
}
return ldapUser;
}
public static User getUserByEmail(IdentityManager idmManager, String email) throws IdentityManagementException {
List<User> agents = idmManager.createIdentityQuery(User.class)
.setParameter(User.EMAIL, email).getResultList();
public static LDAPUser getUserByEmail(LDAPIdentityStore ldapIdentityStore, String email) {
IdentityQueryBuilder queryBuilder = ldapIdentityStore.createQueryBuilder();
IdentityQuery<LDAPUser> query = queryBuilder.createIdentityQuery(LDAPUser.class)
.where(queryBuilder.equal(LDAPUser.EMAIL, email));
List<LDAPUser> users = query.getResultList();
if (agents.isEmpty()) {
if (users.isEmpty()) {
return null;
} else if (agents.size() == 1) {
return agents.get(0);
} else if (users.size() == 1) {
return users.get(0);
} else {
throw new IdentityManagementException("Error - multiple users found with same email");
throw new ModelDuplicateException("Error - multiple users found with same email " + email);
}
}
public static boolean removeUser(PartitionManager partitionManager, String username) {
IdentityManager idmManager = getIdentityManager(partitionManager);
User picketlinkUser = BasicModel.getUser(idmManager, username);
if (picketlinkUser == null) {
public static boolean removeUser(LDAPIdentityStore ldapIdentityStore, String username) {
LDAPUser ldapUser = getUser(ldapIdentityStore, username);
if (ldapUser == null) {
return false;
}
idmManager.remove(picketlinkUser);
ldapIdentityStore.remove(ldapUser);
return true;
}
public static void removeAllUsers(PartitionManager partitionManager) {
IdentityManager idmManager = getIdentityManager(partitionManager);
List<User> users = idmManager.createIdentityQuery(User.class).getResultList();
public static void removeAllUsers(LDAPIdentityStore ldapIdentityStore) {
List<LDAPUser> allUsers = getAllUsers(ldapIdentityStore);
for (User user : users) {
idmManager.remove(user);
for (LDAPUser user : allUsers) {
ldapIdentityStore.remove(user);
}
}
public static List<User> getAllUsers(PartitionManager partitionManager) {
IdentityManager idmManager = getIdentityManager(partitionManager);
return idmManager.createIdentityQuery(User.class).getResultList();
}
private static IdentityManager getIdentityManager(PartitionManager partitionManager) {
return partitionManager.createIdentityManager();
public static List<LDAPUser> getAllUsers(LDAPIdentityStore ldapIdentityStore) {
IdentityQuery<LDAPUser> userQuery = ldapIdentityStore.createQueryBuilder().createIdentityQuery(LDAPUser.class);
return userQuery.getResultList();
}
// Needed for ActiveDirectory updates

View file

@ -1,16 +1,11 @@
package org.keycloak.federation.ldap;
import org.jboss.logging.Logger;
import org.keycloak.models.ModelException;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.picketlink.idm.IdentityManagementException;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.credential.Password;
import org.picketlink.idm.credential.TOTPCredential;
import org.picketlink.idm.model.basic.BasicModel;
import org.picketlink.idm.model.basic.User;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -28,52 +23,43 @@ public class WritableLDAPUserModelDelegate extends UserModelDelegate implements
@Override
public void setUsername(String username) {
IdentityManager identityManager = provider.getIdentityManager();
LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
try {
User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername());
if (ldapUser == null) {
throw new IllegalStateException("User not found in LDAP storage!");
}
picketlinkUser.setLoginName(username);
identityManager.update(picketlinkUser);
} catch (IdentityManagementException ie) {
throw new ModelException(ie);
}
ldapUser.setLoginName(username);
ldapIdentityStore.update(ldapUser);
delegate.setUsername(username);
}
@Override
public void setLastName(String lastName) {
IdentityManager identityManager = provider.getIdentityManager();
LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
try {
User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername());
if (ldapUser == null) {
throw new IllegalStateException("User not found in LDAP storage!");
}
picketlinkUser.setLastName(lastName);
identityManager.update(picketlinkUser);
} catch (IdentityManagementException ie) {
throw new ModelException(ie);
}
ldapUser.setLastName(lastName);
ldapIdentityStore.update(ldapUser);
delegate.setLastName(lastName);
}
@Override
public void setFirstName(String first) {
IdentityManager identityManager = provider.getIdentityManager();
LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
try {
User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername());
if (ldapUser == null) {
throw new IllegalStateException("User not found in LDAP storage!");
}
picketlinkUser.setFirstName(first);
identityManager.update(picketlinkUser);
} catch (IdentityManagementException ie) {
throw new ModelException(ie);
}
ldapUser.setFirstName(first);
ldapIdentityStore.update(ldapUser);
delegate.setFirstName(first);
}
@ -83,41 +69,31 @@ public class WritableLDAPUserModelDelegate extends UserModelDelegate implements
delegate.updateCredential(cred);
return;
}
IdentityManager identityManager = provider.getIdentityManager();
try {
User picketlinkUser = BasicModel.getUser(identityManager, getUsername());
if (picketlinkUser == null) {
logger.debugf("User '%s' doesn't exists. Skip password update", getUsername());
throw new IllegalStateException("User doesn't exist in LDAP storage");
LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername());
if (ldapUser == null) {
throw new IllegalStateException("User " + delegate.getUsername() + " not found in LDAP storage!");
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
identityManager.updateCredential(picketlinkUser, new Password(cred.getValue().toCharArray()));
} else if (cred.getType().equals(UserCredentialModel.TOTP)) {
TOTPCredential credential = new TOTPCredential(cred.getValue());
credential.setDevice(cred.getDevice());
identityManager.updateCredential(picketlinkUser, credential);
LDAPUtils.updatePassword(ldapIdentityStore, delegate, cred.getValue());
} else {
logger.warnf("Don't know how to update credential of type [%s] for user [%s]", cred.getType(), delegate.getUsername());
}
} catch (IdentityManagementException ie) {
throw new ModelException(ie);
}
}
@Override
public void setEmail(String email) {
IdentityManager identityManager = provider.getIdentityManager();
LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
try {
User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
if (picketlinkUser == null) {
LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername());
if (ldapUser == null) {
throw new IllegalStateException("User not found in LDAP storage!");
}
picketlinkUser.setEmail(email);
identityManager.update(picketlinkUser);
} catch (IdentityManagementException ie) {
throw new ModelException(ie);
}
ldapUser.setEmail(email);
ldapIdentityStore.update(ldapUser);
delegate.setEmail(email);
}

View file

@ -0,0 +1,85 @@
package org.keycloak.federation.ldap.idm.model;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableMap;
/**
* Abstract base class for all AttributedType implementations
*
* @author Shane Bryzak
*
*/
public abstract class AbstractAttributedType implements AttributedType {
private static final long serialVersionUID = -6118293036241099199L;
private String id;
private String entryDN;
private Map<String, Attribute<? extends Serializable>> attributes =
new HashMap<String, Attribute<? extends Serializable>>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getEntryDN() {
return entryDN;
}
public void setEntryDN(String entryDN) {
this.entryDN = entryDN;
}
public void setAttribute(Attribute<? extends Serializable> attribute) {
attributes.put(attribute.getName(), attribute);
}
public void removeAttribute(String name) {
attributes.remove(name);
}
@SuppressWarnings("unchecked")
public <T extends Serializable> Attribute<T> getAttribute(String name) {
return (Attribute<T>) attributes.get(name);
}
public Collection<Attribute<? extends Serializable>> getAttributes() {
return unmodifiableCollection(attributes.values());
}
public Map<String,Attribute<? extends Serializable>> getAttributesMap() {
return unmodifiableMap(attributes);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!getClass().isInstance(obj)) {
return false;
}
AttributedType other = (AttributedType) obj;
return getId() != null && other.getId() != null && getId().equals(other.getId());
}
@Override
public int hashCode() {
int result = getId() != null ? getId().hashCode() : 0;
result = 31 * result + (getId() != null ? getId().hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,70 @@
package org.keycloak.federation.ldap.idm.model;
import java.util.Date;
/**
* Abstract base class for IdentityType implementations
*
* @author Shane Bryzak
*/
public abstract class AbstractIdentityType extends AbstractAttributedType implements IdentityType {
private static final long serialVersionUID = 2843998332737143820L;
private boolean enabled = true;
private Date createdDate = new Date();
private Date expirationDate = null;
public boolean isEnabled() {
return this.enabled;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
@AttributeProperty
public Date getExpirationDate() {
return this.expirationDate;
}
@Override
public void setExpirationDate(Date expirationDate) {
this.expirationDate = expirationDate;
}
@Override
@AttributeProperty
public Date getCreatedDate() {
return this.createdDate;
}
@Override
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!getClass().isInstance(obj)) {
return false;
}
IdentityType other = (IdentityType) obj;
return (getId() != null && other.getId() != null)
&& (getId().equals(other.getId()));
}
@Override
public int hashCode() {
return super.hashCode();
}
}

View file

@ -0,0 +1,80 @@
package org.keycloak.federation.ldap.idm.model;
import java.io.Serializable;
/**
* Represents an attribute value, a type of metadata that can be associated with an IdentityType
*
* @author Shane Bryzak
*
* @param <T>
*/
public class Attribute<T extends Serializable> implements Serializable {
private static final long serialVersionUID = 237211288303510728L;
/**
* The name of the attribute
*/
private String name;
/**
* The attribute value.
*/
private T value;
/**
* Indicates whether this Attribute has a read-only value
*/
private boolean readOnly = false;
/**
* Indicates whether the Attribute value has been loaded
*/
private boolean loaded = false;
public Attribute(String name, T value) {
this.name = name;
this.value = value;
}
public Attribute(String name, T value, boolean readOnly) {
this(name, value);
this.readOnly = readOnly;
}
public String getName() {
return name;
}
public T getValue() {
return value;
}
public boolean isReadOnly() {
return readOnly;
}
public boolean isLoaded() {
return loaded;
}
public void setLoaded(boolean value) {
this.loaded = value;
}
/**
* Sets the value for this attribute. If the Attribute value is readOnly, a RuntimeException is thrown.
*
* @param value
*/
public void setValue(T value) {
if (readOnly) {
throw new RuntimeException("Error setting Attribute value [" + name + " ] - value is read only.");
}
this.value = value;
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.federation.ldap.idm.model;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marks a property of an IdentityType, Partition or Relationship as being an attribute of that
* IdentityType, Partition or Relationship.
*
* @author Shane Bryzak
*/
@Target({METHOD, FIELD})
@Documented
@Retention(RUNTIME)
@Inherited
public @interface AttributeProperty {
/**
* <p>Managed properties are stored as ad-hoc attributes and mapped from and to a specific property of a type.</p>
*
* @return
*/
boolean managed() default false;
}

View file

@ -0,0 +1,75 @@
package org.keycloak.federation.ldap.idm.model;
import java.io.Serializable;
import java.util.Collection;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
*
* @author Shane Bryzak
*
*/
public interface AttributedType extends Serializable {
/**
* A query parameter used to set the id value.
*/
QueryParameter ID = new AttributeParameter("id");
/**
* Returns the unique identifier for this instance
* @return
*/
String getId();
/**
* Sets the unique identifier for this instance
* @return
*/
void setId(String id);
/**
* Set the specified attribute. This operation will overwrite any previous value.
*
* @param attribute to be set
*/
void setAttribute(Attribute<? extends Serializable> attribute);
/**
* Remove the attribute with given name
*
* @param name of attribute
*/
void removeAttribute(String name);
// LDAP specific stuff
void setEntryDN(String entryDN);
String getEntryDN();
/**
* Return the attribute value with the specified name
*
* @param name of attribute
* @return attribute value or null if attribute with given name doesn't exist. If given attribute has many values method
* will return first one
*/
<T extends Serializable> Attribute<T> getAttribute(String name);
/**
* Returns a Map containing all attribute values for this IdentityType instance.
*
* @return map of attribute names and their values
*/
Collection<Attribute<? extends Serializable>> getAttributes();
public final class QUERY_ATTRIBUTE {
public static AttributeParameter byName(String name) {
return new AttributeParameter(name);
}
}
}

View file

@ -0,0 +1,100 @@
package org.keycloak.federation.ldap.idm.model;
import java.util.Date;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* This interface is the base for all identity model objects. It declares a number of
* properties that must be supported by all identity types, in addition to defining the API
* for identity attribute management.
*
* @author Shane Bryzak
*/
public interface IdentityType extends AttributedType {
/**
* A query parameter used to set the enabled value.
*/
QueryParameter ENABLED = new AttributeParameter("enabled");
/**
* A query parameter used to set the createdDate value
*/
QueryParameter CREATED_DATE = new AttributeParameter("createdDate");
/**
* A query parameter used to set the created after date
*/
QueryParameter CREATED_AFTER = new AttributeParameter("createdDate");
/**
* A query parameter used to set the modified after date
*/
QueryParameter MODIFIED_AFTER = new AttributeParameter("modifyDate");
/**
* A query parameter used to set the created before date
*/
QueryParameter CREATED_BEFORE = new AttributeParameter("createdDate");
/**
* A query parameter used to set the expiryDate value
*/
QueryParameter EXPIRY_DATE = new AttributeParameter("expirationDate");
/**
* A query parameter used to set the expiration after date
*/
QueryParameter EXPIRY_AFTER = new AttributeParameter("expirationDate");
/**
* A query parameter used to set the expiration before date
*/
QueryParameter EXPIRY_BEFORE = new AttributeParameter("expirationDate");
/**
* Indicates the current enabled status of this IdentityType.
*
* @return A boolean value indicating whether this IdentityType is enabled.
*/
boolean isEnabled();
/**
* <p>Sets the current enabled status of this {@link IdentityType}.</p>
*
* @param enabled
*/
void setEnabled(boolean enabled);
/**
* Returns the date that this IdentityType instance was created.
*
* @return Date value representing the creation date
*/
Date getCreatedDate();
/**
* <p>Sets the date that this {@link IdentityType} was created.</p>
*
* @param createdDate
*/
void setCreatedDate(Date createdDate);
/**
* Returns the date that this IdentityType expires, or null if there is no expiry date.
*
* @return
*/
Date getExpirationDate();
/**
* <p>Sets the date that this {@link IdentityType} expires.</p>
*
* @param expirationDate
*/
void setExpirationDate(Date expirationDate);
}

View file

@ -0,0 +1,85 @@
package org.keycloak.federation.ldap.idm.model;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* This class represents a User; a human agent that may authenticate with the application
*
* @author Shane Bryzak
*/
public class LDAPUser extends AbstractIdentityType {
private static final long serialVersionUID = 4117586097100398485L;
public static final QueryParameter LOGIN_NAME = AttributedType.QUERY_ATTRIBUTE.byName("loginName");
/**
* A query parameter used to set the firstName value.
*/
public static final QueryParameter FIRST_NAME = QUERY_ATTRIBUTE.byName("firstName");
/**
* A query parameter used to set the lastName value.
*/
public static final QueryParameter LAST_NAME = QUERY_ATTRIBUTE.byName("lastName");
/**
* A query parameter used to set the email value.
*/
public static final QueryParameter EMAIL = QUERY_ATTRIBUTE.byName("email");
@AttributeProperty
private String loginName;
@AttributeProperty
private String firstName;
@AttributeProperty
private String lastName;
@AttributeProperty
private String email;
public LDAPUser() {
}
public LDAPUser(String loginName) {
this.loginName = loginName;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.federation.ldap.idm.query;
/**
* <p>This class can be used to define a query parameter for properties annotated with
* {@link org.keycloak.federation.ldap.idm.model.AttributeProperty}.
* </p>
*
* @author pedroigor
*/
public class AttributeParameter implements QueryParameter {
private final String name;
public AttributeParameter(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}

View file

@ -0,0 +1,18 @@
package org.keycloak.federation.ldap.idm.query;
/**
* <p>A {@link Condition} is used to specify how a specific {@link QueryParameter}
* is defined in order to filter query results.</p>
*
* @author Pedro Igor
*/
public interface Condition {
/**
* <p>The {@link QueryParameter} restricted by this condition.</p>
*
* @return
*/
QueryParameter getParameter();
}

View file

@ -0,0 +1,225 @@
package org.keycloak.federation.ldap.idm.query;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.federation.ldap.idm.model.IdentityType;
/**
* <p>An {@link IdentityQuery} is responsible for querying the underlying identity stores for instances of
* a given {@link IdentityType}.</p>
*
* <p>Instances of this class are obtained using the {@link IdentityQueryBuilder#createIdentityQuery(Class)}
* method.</p>
*
* <pre>
* IdentityManager identityManager = getIdentityManager();
*
* // here we get the query builder
* IdentityQueryBuilder builder = identityManager.getQueryBuilder();
*
* // create a condition
* Condition condition = builder.equal(User.LOGIN_NAME, "john");
*
* // create a query for a specific identity type using the previously created condition
* IdentityQuery query = builder.createIdentityQuery(User.class).where(condition);
*
* // execute the query
* List<User> result = query.getResultList();
* </pre>
*
* <p>When preparing a query you may want to create conditions to filter its results and configure how they must be retrieved.
* For that, you can use the {@link IdentityQueryBuilder}, which provides useful methods for creating
* different expressions and conditions.</p>
*
* @author Shane Bryzak
* @author Pedro Igor
*/
public interface IdentityQuery<T extends IdentityType> {
/**
* @see #setPaginationContext(Object object)
*/
Object getPaginationContext();
/**
* Used for pagination models like LDAP when search will return some object (like cookie) for searching on next page
*
* @param object to be used for search next page
*
* @return this query
*/
IdentityQuery<T> setPaginationContext(Object object);
/**
* @deprecated Will be removed soon.
*
* @see #setSortParameters(QueryParameter...)
*/
@Deprecated
QueryParameter[] getSortParameters();
/**
* Parameters used to sort the results. First parameter has biggest priority. For example: setSortParameter(User.LAST_NAME,
* User.FIRST_NAME) means that results will be sorted primarily by lastName and firstName will be used to sort only records with
* same lastName
*
* @param sortParameters parameters to specify sort criteria
*
* @deprecated Use {@link IdentityQuery#sortBy(Sort...)} instead. Where you can create sort conditions
* from the {@link IdentityQueryBuilder}.
*
* @return this query
*/
@Deprecated
IdentityQuery<T> setSortParameters(QueryParameter... sortParameters);
/**
* @deprecated Use {@link IdentityQuery#getSorting()} for a list of sorting conditions. Will be removed soon.
*
* @return true if sorting will be ascending
*
* @see #setSortAscending(boolean)
*/
@Deprecated
boolean isSortAscending();
/**
* Specify if sorting will be ascending (true) or descending (false)
*
* @param sortAscending to specify if sorting will be ascending or descending
*
* @deprecated Use {@link IdentityQuery#sortBy(Sort...)} instead. Where you can create sort conditions
* from the {@link IdentityQueryBuilder}.
*
* @return this query
*/
@Deprecated
IdentityQuery<T> setSortAscending(boolean sortAscending);
/**
* <p>Set a query parameter to this query in order to filter the results.</p>
*
* <p>This method always create an equality condition. For more conditions options take a look at {@link
* IdentityQueryBuilder} and use the {@link IdentityQuery#where(Condition...)}
* instead.</p>
*
* @param param The query parameter.
* @param value The value to match for equality.
*
* @return
*
* @deprecated Use {@link IdentityQuery#where(Condition...)} to specify query conditions.
*/
@Deprecated
IdentityQuery<T> setParameter(QueryParameter param, Object... value);
/**
* <p>Add to this query the conditions that will be used to filter results.</p>
*
* <p>Any condition previously added to this query will be preserved and the new conditions added. If you want to clear the
* conditions you must create a new query instance.</p>
*
* @param condition One or more conditions created from {@link IdentityQueryBuilder}.
*
* @return
*/
IdentityQuery<T> where(Condition... condition);
/**
* <p>Add to this query the sorting conditions to be applied to the results.</p>
*
* @param sorts The ordering conditions.
*
* @return
*/
IdentityQuery<T> sortBy(Sort... sorts);
/**
* <p>The type used to create this query.</p>
*
* @return
*/
Class<T> getIdentityType();
/**
* <p>Returns a map with all the parameter set for this query.</p>
*
* @return
*
* @deprecated Use {@link IdentityQuery#getConditions()} instead. Will be removed.
*/
@Deprecated
Map<QueryParameter, Object[]> getParameters();
/**
* <p>Returns a set containing all conditions used by this query to filter its results.</p>
*
* @return
*/
Set<Condition> getConditions();
/**
* <p>Returns a set containing all sorting conditions used to filter the results.</p>
*
* @return
*/
Set<Sort> getSorting();
/**
* <p>Returns the value used to restrict the given query parameter.</p>
*
* @param queryParameter
*
* @return
*/
@Deprecated
Object[] getParameter(QueryParameter queryParameter);
@Deprecated
Map<QueryParameter, Object[]> getParameters(Class<?> type);
int getOffset();
/**
* <p>Set the position of the first result to retrieve.</p>
*
* @param offset
*
* @return
*/
IdentityQuery<T> setOffset(int offset);
/**
* <p>Returns the number of instances to retrieve.</p>
*
* @return
*/
int getLimit();
/**
* <p>Set the maximum number of results to retrieve.</p>
*
* @param limit the number of instances to retrieve.
*
* @return
*/
IdentityQuery<T> setLimit(int limit);
/**
* <p>Execute the query against the underlying identity stores and returns a list containing all instances of
* the type (defined when creating this query instance) that match the conditions previously specified.</p>
*
* @return
*/
List<T> getResultList();
/**
* Count of all query results. It takes into account query parameters, but it doesn't take into account pagination parameter
* like offset and limit
*
* @return count of all query results
*/
int getResultCount();
}

View file

@ -0,0 +1,124 @@
package org.keycloak.federation.ldap.idm.query;
import org.keycloak.federation.ldap.idm.model.IdentityType;
/**
* <p>The {@link IdentityQueryBuilder} is responsible for creating {@link IdentityQuery} instances and also
* provide methods to create conditions, orderings, sorting, etc.</p>
*
* @author Pedro Igor
*/
public interface IdentityQueryBuilder {
/**
* <p>Create a condition for testing the whether the query parameter satisfies the given pattern..</p>
*
* @param parameter The query parameter.
* @param pattern The pattern to match.
*
* @return
*/
Condition like(QueryParameter parameter, String pattern);
/**
* <p>Create a condition for testing the arguments for equality.</p>
*
* @param parameter The query parameter.
* @param value The value to compare.
*
* @return
*/
Condition equal(QueryParameter parameter, Object value);
/**
* <p>Create a condition for testing whether the query parameter is grater than the given value..</p>
*
* @param parameter The query parameter.
* @param x The value to compare.
*
* @return
*/
Condition greaterThan(QueryParameter parameter, Object x);
/**
* <p>Create a condition for testing whether the query parameter is grater than or equal to the given value..</p>
*
* @param parameter The query parameter.
* @param x The value to compare.
*
* @return
*/
Condition greaterThanOrEqualTo(QueryParameter parameter, Object x);
/**
* <p>Create a condition for testing whether the query parameter is less than the given value..</p>
*
* @param parameter The query parameter.
* @param x The value to compare.
*
* @return
*/
Condition lessThan(QueryParameter parameter, Object x);
/**
* <p>Create a condition for testing whether the query parameter is less than or equal to the given value..</p>
*
* @param parameter The query parameter.
* @param x The value to compare.
*
* @return
*/
Condition lessThanOrEqualTo(QueryParameter parameter, Object x);
/**
* <p>Create a condition for testing whether the query parameter is between the given values.</p>
*
* @param parameter The query parameter.
* @param x The first value.
* @param x The second value.
*
* @return
*/
Condition between(QueryParameter parameter, Object x, Object y);
/**
* <p>Create a condition for testing whether the query parameter is contained in a list of values.</p>
*
* @param parameter The query parameter.
* @param values A list of values.
*
* @return
*/
Condition in(QueryParameter parameter, Object... values);
/**
* <p>Create an ascending order for the given <code>parameter</code>. Once created, you can use it to sort the results of a
* query.</p>
*
* @param parameter The query parameter to sort.
*
* @return
*/
Sort asc(QueryParameter parameter);
/**
* <p>Create an descending order for the given <code>parameter</code>. Once created, you can use it to sort the results of a
* query.</p>
*
* @param parameter The query parameter to sort.
*
* @return
*/
Sort desc(QueryParameter parameter);
/**
* <p> Create an {@link IdentityQuery} that can be used to query for {@link
* IdentityType} instances of a the given <code>identityType</code>. </p>
*
* @param identityType The type to search. If you provide the {@link IdentityType}
* base interface any of its sub-types will be returned.
*
* @return
*/
<T extends IdentityType> IdentityQuery<T> createIdentityQuery(Class<T> identityType);
}

View file

@ -0,0 +1,12 @@
package org.keycloak.federation.ldap.idm.query;
/**
* A marker interface indicating that the implementing class can be used as a
* parameter within an IdentityQuery or RelationshipQuery
*
* @author Shane Bryzak
*
*/
public interface QueryParameter {
}

View file

@ -0,0 +1,23 @@
package org.keycloak.federation.ldap.idm.query;
/**
* @author Pedro Igor
*/
public class Sort {
private final QueryParameter parameter;
private final boolean asc;
public Sort(QueryParameter parameter, boolean asc) {
this.parameter = parameter;
this.asc = asc;
}
public QueryParameter getParameter() {
return this.parameter;
}
public boolean isAscending() {
return asc;
}
}

View file

@ -0,0 +1,33 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class BetweenCondition implements Condition {
private final Comparable x;
private final Comparable y;
private final QueryParameter parameter;
public BetweenCondition(QueryParameter parameter, Comparable x, Comparable y) {
this.parameter = parameter;
this.x = x;
this.y = y;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Comparable getX() {
return this.x;
}
public Comparable getY() {
return this.y;
}
}

View file

@ -0,0 +1,207 @@
package org.keycloak.federation.ldap.idm.query.internal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.Sort;
import org.keycloak.federation.ldap.idm.store.IdentityStore;
import org.keycloak.models.ModelException;
import static java.util.Collections.unmodifiableSet;
/**
* Default IdentityQuery implementation.
*
* @param <T>
*
* @author Shane Bryzak
*/
public class DefaultIdentityQuery<T extends IdentityType> implements IdentityQuery<T> {
private final Map<QueryParameter, Object[]> parameters = new LinkedHashMap<QueryParameter, Object[]>();
private final Class<T> identityType;
private final IdentityStore identityStore;
private final IdentityQueryBuilder queryBuilder;
private int offset;
private int limit;
private Object paginationContext;
private QueryParameter[] sortParameters;
private boolean sortAscending = true;
private final Set<Condition> conditions = new LinkedHashSet<Condition>();
private final Set<Sort> ordering = new LinkedHashSet<Sort>();
public DefaultIdentityQuery(IdentityQueryBuilder queryBuilder, Class<T> identityType, IdentityStore identityStore) {
this.queryBuilder = queryBuilder;
this.identityStore = identityStore;
this.identityType = identityType;
}
@Override
public IdentityQuery<T> setParameter(QueryParameter queryParameter, Object... value) {
if (value == null || value.length == 0) {
throw new ModelException("Query Parameter values null or empty");
}
parameters.put(queryParameter, value);
if (IdentityType.CREATED_AFTER.equals(queryParameter) || IdentityType.EXPIRY_AFTER.equals(queryParameter)) {
this.conditions.add(queryBuilder.greaterThanOrEqualTo(queryParameter, value[0]));
} else if (IdentityType.CREATED_BEFORE.equals(queryParameter) || IdentityType.EXPIRY_BEFORE.equals(queryParameter)) {
this.conditions.add(queryBuilder.lessThanOrEqualTo(queryParameter, value[0]));
} else {
this.conditions.add(queryBuilder.equal(queryParameter, value[0]));
}
return this;
}
@Override
public IdentityQuery<T> where(Condition... condition) {
this.conditions.addAll(Arrays.asList(condition));
return this;
}
@Override
public IdentityQuery<T> sortBy(Sort... sorts) {
this.ordering.addAll(Arrays.asList(sorts));
return this;
}
@Override
public Set<Sort> getSorting() {
return unmodifiableSet(this.ordering);
}
@Override
public Class<T> getIdentityType() {
return identityType;
}
@Override
public Map<QueryParameter, Object[]> getParameters() {
return parameters;
}
@Override
public Object[] getParameter(QueryParameter queryParameter) {
return this.parameters.get(queryParameter);
}
@Override
public Map<QueryParameter, Object[]> getParameters(Class<?> type) {
Map<QueryParameter, Object[]> typedParameters = new HashMap<QueryParameter, Object[]>();
Set<Map.Entry<QueryParameter, Object[]>> entrySet = this.parameters.entrySet();
for (Map.Entry<QueryParameter, Object[]> entry : entrySet) {
if (type.isInstance(entry.getKey())) {
typedParameters.put(entry.getKey(), entry.getValue());
}
}
return typedParameters;
}
@Override
public int getLimit() {
return limit;
}
@Override
public int getOffset() {
return offset;
}
@Override
public Object getPaginationContext() {
return paginationContext;
}
@Override
public QueryParameter[] getSortParameters() {
return sortParameters;
}
@Override
public boolean isSortAscending() {
return sortAscending;
}
@Override
public List<T> getResultList() {
// remove this statement once deprecated methods on IdentityQuery are removed
if (this.sortParameters != null) {
for (QueryParameter parameter : this.sortParameters) {
if (isSortAscending()) {
sortBy(this.queryBuilder.asc(parameter));
} else {
sortBy(this.queryBuilder.desc(parameter));
}
}
}
List<T> result = new ArrayList<T>();
try {
for (T identityType : identityStore.fetchQueryResults(this)) {
result.add(identityType);
}
} catch (Exception e) {
throw new ModelException("LDAP Query failed", e);
}
return result;
}
@Override
public int getResultCount() {
return identityStore.countQueryResults(this);
}
@Override
public IdentityQuery<T> setOffset(int offset) {
this.offset = offset;
return this;
}
@Override
public IdentityQuery<T> setLimit(int limit) {
this.limit = limit;
return this;
}
@Override
public IdentityQuery<T> setSortParameters(QueryParameter... sortParameters) {
this.sortParameters = sortParameters;
return this;
}
@Override
public IdentityQuery<T> setSortAscending(boolean sortAscending) {
this.sortAscending = sortAscending;
return this;
}
@Override
public IdentityQuery<T> setPaginationContext(Object object) {
this.paginationContext = object;
return this;
}
@Override
public Set<Condition> getConditions() {
return unmodifiableSet(this.conditions);
}
}

View file

@ -0,0 +1,89 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.Sort;
import org.keycloak.federation.ldap.idm.store.IdentityStore;
import org.keycloak.models.ModelException;
/**
* @author Pedro Igor
*/
public class DefaultQueryBuilder implements IdentityQueryBuilder {
private final IdentityStore identityStore;
public DefaultQueryBuilder(IdentityStore identityStore) {
this.identityStore = identityStore;
}
@Override
public Condition like(QueryParameter parameter, String pattern) {
return new LikeCondition(parameter, pattern);
}
@Override
public Condition equal(QueryParameter parameter, Object value) {
return new EqualCondition(parameter, value);
}
@Override
public Condition greaterThan(QueryParameter parameter, Object x) {
throwExceptionIfNotComparable(x);
return new GreaterThanCondition(parameter, (Comparable) x, false);
}
@Override
public Condition greaterThanOrEqualTo(QueryParameter parameter, Object x) {
throwExceptionIfNotComparable(x);
return new GreaterThanCondition(parameter, (Comparable) x, true);
}
@Override
public Condition lessThan(QueryParameter parameter, Object x) {
throwExceptionIfNotComparable(x);
return new LessThanCondition(parameter, (Comparable) x, false);
}
@Override
public Condition lessThanOrEqualTo(QueryParameter parameter, Object x) {
throwExceptionIfNotComparable(x);
return new LessThanCondition(parameter, (Comparable) x, true);
}
@Override
public Condition between(QueryParameter parameter, Object x, Object y) {
throwExceptionIfNotComparable(x);
throwExceptionIfNotComparable(y);
return new BetweenCondition(parameter, (Comparable) x, (Comparable) y);
}
@Override
public Condition in(QueryParameter parameter, Object... x) {
return new InCondition(parameter, x);
}
@Override
public Sort asc(QueryParameter parameter) {
return new Sort(parameter, true);
}
@Override
public Sort desc(QueryParameter parameter) {
return new Sort(parameter, false);
}
@Override
public <T extends IdentityType> IdentityQuery createIdentityQuery(Class<T> identityType) {
return new DefaultIdentityQuery(this, identityType, this.identityStore);
}
private void throwExceptionIfNotComparable(Object x) {
if (!Comparable.class.isInstance(x)) {
throw new ModelException("Query parameter value [" + x + "] must be " + Comparable.class + ".");
}
}
}

View file

@ -0,0 +1,36 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class EqualCondition implements Condition {
private final QueryParameter parameter;
private final Object value;
public EqualCondition(QueryParameter parameter, Object value) {
this.parameter = parameter;
this.value = value;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Object getValue() {
return this.value;
}
@Override
public String toString() {
return "EqualCondition{" +
"parameter=" + ((AttributeParameter) parameter).getName() +
", value=" + value +
'}';
}
}

View file

@ -0,0 +1,34 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class GreaterThanCondition implements Condition {
private final boolean orEqual;
private final QueryParameter parameter;
private final Comparable value;
public GreaterThanCondition(QueryParameter parameter, Comparable value, boolean orEqual) {
this.parameter = parameter;
this.value = value;
this.orEqual = orEqual;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Comparable getValue() {
return this.value;
}
public boolean isOrEqual() {
return this.orEqual;
}
}

View file

@ -0,0 +1,28 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class InCondition implements Condition {
private final QueryParameter parameter;
private final Object[] value;
public InCondition(QueryParameter parameter, Object[] value) {
this.parameter = parameter;
this.value = value;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Object[] getValue() {
return this.value;
}
}

View file

@ -0,0 +1,34 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class LessThanCondition implements Condition {
private final boolean orEqual;
private final QueryParameter parameter;
private final Comparable value;
public LessThanCondition(QueryParameter parameter, Comparable value, boolean orEqual) {
this.parameter = parameter;
this.value = value;
this.orEqual = orEqual;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Comparable getValue() {
return this.value;
}
public boolean isOrEqual() {
return this.orEqual;
}
}

View file

@ -0,0 +1,28 @@
package org.keycloak.federation.ldap.idm.query.internal;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
/**
* @author Pedro Igor
*/
public class LikeCondition implements Condition {
private final QueryParameter parameter;
private final Object value;
public LikeCondition(QueryParameter parameter, Object value) {
this.parameter = parameter;
this.value = value;
}
@Override
public QueryParameter getParameter() {
return this.parameter;
}
public Object getValue() {
return this.value;
}
}

View file

@ -0,0 +1,81 @@
package org.keycloak.federation.ldap.idm.store;
import java.util.List;
import org.keycloak.federation.ldap.idm.model.AttributedType;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStoreConfiguration;
/**
* IdentityStore representation providing minimal SPI
*
* TODO: Rather remove this abstraction
*
* @author Boleslaw Dawidowicz
* @author Shane Bryzak
*/
public interface IdentityStore {
/**
* Returns the configuration for this IdentityStore instance
*
* @return
*/
LDAPIdentityStoreConfiguration getConfig();
// General
/**
* Persists the specified IdentityType
*
* @param value
*/
void add(AttributedType value);
/**
* Updates the specified IdentityType
*
* @param value
*/
void update(AttributedType value);
/**
* Removes the specified IdentityType
*
* @param value
*/
void remove(AttributedType value);
// Identity query
<V extends IdentityType> List<V> fetchQueryResults(IdentityQuery<V> identityQuery);
<V extends IdentityType> int countQueryResults(IdentityQuery<V> identityQuery);
// // Relationship query
//
// <V extends Relationship> List<V> fetchQueryResults(RelationshipQuery<V> query);
//
// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);
// Credentials
/**
* Validates the specified credentials.
*
* @param user Keycloak user
* @param password Ldap password
*/
boolean validatePassword(LDAPUser user, String password);
/**
* Updates the specified credential value.
*
* @param user Keycloak user
* @param password Ldap password
*/
void updatePassword(LDAPUser user, String password);
}

View file

@ -0,0 +1,761 @@
package org.keycloak.federation.ldap.idm.store.ldap;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchResult;
import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.idm.model.AttributedType;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.internal.BetweenCondition;
import org.keycloak.federation.ldap.idm.query.internal.DefaultQueryBuilder;
import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.InCondition;
import org.keycloak.federation.ldap.idm.query.internal.LessThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.LikeCondition;
import org.keycloak.federation.ldap.idm.store.IdentityStore;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.reflection.NamedPropertyCriteria;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.models.utils.reflection.TypedPropertyCriteria;
import org.keycloak.util.reflections.Reflections;
/**
* An IdentityStore implementation backed by an LDAP directory
*
* @author Shane Bryzak
* @author Anil Saldhana
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
*/
public class LDAPIdentityStore implements IdentityStore {
private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
public static final String EMPTY_ATTRIBUTE_VALUE = " ";
private final LDAPIdentityStoreConfiguration config;
private final LDAPOperationManager operationManager;
public LDAPIdentityStore(LDAPIdentityStoreConfiguration config) {
this.config = config;
try {
this.operationManager = new LDAPOperationManager(getConfig());
} catch (NamingException e) {
throw new ModelException("Couldn't init operation manager", e);
}
}
@Override
public LDAPIdentityStoreConfiguration getConfig() {
return this.config;
}
@Override
public void add(AttributedType attributedType) {
// id will be assigned by the ldap server
attributedType.setId(null);
String entryDN = getBindingDN(attributedType, true);
this.operationManager.createSubContext(entryDN, extractAttributes(attributedType, true));
addToParentAsMember(attributedType);
attributedType.setId(getEntryIdentifier(attributedType));
attributedType.setEntryDN(entryDN);
if (logger.isTraceEnabled()) {
logger.tracef("Type with identifier [%s] successfully added to identity store [%s].", attributedType.getId(), this);
}
}
@Override
public void update(AttributedType attributedType) {
BasicAttributes updatedAttributes = extractAttributes(attributedType, false);
NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
this.operationManager.modifyAttributes(getBindingDN(attributedType, true), attributes);
if (logger.isTraceEnabled()) {
logger.tracef("Type with identifier [%s] successfully updated to identity store [%s].", attributedType.getId(), this);
}
}
@Override
public void remove(AttributedType attributedType) {
LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
this.operationManager.removeEntryById(getBaseDN(attributedType), attributedType.getId(), mappingConfig);
if (logger.isTraceEnabled()) {
logger.tracef("Type with identifier [%s] successfully removed from identity store [%s].", attributedType.getId(), this);
}
}
@Override
public <V extends IdentityType> List<V> fetchQueryResults(IdentityQuery<V> identityQuery) {
List<V> results = new ArrayList<V>();
try {
if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
throw new ModelException("LDAP Identity Store does not support sorted queries.");
}
for (Condition condition : identityQuery.getConditions()) {
if (IdentityType.ID.equals(condition.getParameter())) {
if (EqualCondition.class.isInstance(condition)) {
EqualCondition equalCondition = (EqualCondition) condition;
SearchResult search = this.operationManager
.lookupById(getConfig().getBaseDN(), equalCondition.getValue().toString(), null);
if (search != null) {
results.add((V) populateAttributedType(search, null));
}
}
return results;
}
}
if (!IdentityType.class.equals(identityQuery.getIdentityType())) {
// the ldap store does not support queries based on root types. Except if based on the identifier.
LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(identityQuery.getIdentityType());
StringBuilder filter = createIdentityTypeSearchFilter(identityQuery, ldapEntryConfig);
String baseDN = getBaseDN(ldapEntryConfig);
List<SearchResult> search;
if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
search = this.operationManager.searchPaginated(baseDN, filter.toString(), ldapEntryConfig, identityQuery);
} else {
search = this.operationManager.search(baseDN, filter.toString(), ldapEntryConfig);
}
for (SearchResult result : search) {
if (!result.getNameInNamespace().equals(baseDN)) {
results.add((V) populateAttributedType(result, null));
}
}
}
} catch (Exception e) {
throw new ModelException("Querying of identity type failed " + identityQuery, e);
}
return results;
}
@Override
public <V extends IdentityType> int countQueryResults(IdentityQuery<V> identityQuery) {
int limit = identityQuery.getLimit();
int offset = identityQuery.getOffset();
identityQuery.setLimit(0);
identityQuery.setOffset(0);
int resultCount = identityQuery.getResultList().size();
identityQuery.setLimit(limit);
identityQuery.setOffset(offset);
return resultCount;
}
public IdentityQueryBuilder createQueryBuilder() {
return new DefaultQueryBuilder(this);
}
// *************** CREDENTIALS AND USER SPECIFIC STUFF
@Override
public boolean validatePassword(LDAPUser user, String password) {
String userDN = getEntryDNOfUser(user);
if (logger.isDebugEnabled()) {
logger.debugf("Using DN [%s] for authentication of user [%s]", userDN, user.getLoginName());
}
if (operationManager.authenticate(userDN, password)) {
return true;
}
return false;
}
@Override
public void updatePassword(LDAPUser user, String password) {
String userDN = getEntryDNOfUser(user);
if (logger.isDebugEnabled()) {
logger.debugf("Using DN [%s] for updating LDAP password of user [%s]", userDN, user.getLoginName());
}
if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password);
} else {
ModificationItem[] mods = new ModificationItem[1];
try {
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttribute(userDN, mod0);
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
}
}
private void updateADPassword(String userDN, String password) {
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);
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
// 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 (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));
logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
}
operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
} catch (Exception e) {
throw new ModelException(e);
}
}
private String getEntryDNOfUser(LDAPUser user) {
// First try if user already has entryDN on him
String entryDN = user.getEntryDN();
if (entryDN != null) {
return entryDN;
}
// Need to find user in LDAP
String username = user.getLoginName();
user = getUser(username);
if (user == null) {
throw new ModelException("No LDAP user found with username " + username);
}
return user.getEntryDN();
}
public LDAPUser getUser(String username) {
if (isNullOrEmpty(username)) {
return null;
}
IdentityQueryBuilder queryBuilder = createQueryBuilder();
List<LDAPUser> agents = queryBuilder.createIdentityQuery(LDAPUser.class)
.where(queryBuilder.equal(LDAPUser.LOGIN_NAME, username)).getResultList();
if (agents.isEmpty()) {
return null;
} else if (agents.size() == 1) {
return agents.get(0);
} else {
throw new ModelDuplicateException("Error - multiple Agent objects found with same login name");
}
}
// ************ END CREDENTIALS AND USER SPECIFIC STUFF
private String getBaseDN(final LDAPMappingConfiguration ldapEntryConfig) {
String baseDN = getConfig().getBaseDN();
if (ldapEntryConfig.getBaseDN() != null) {
baseDN = ldapEntryConfig.getBaseDN();
}
return baseDN;
}
protected <V extends IdentityType> StringBuilder createIdentityTypeSearchFilter(final IdentityQuery<V> identityQuery, final LDAPMappingConfiguration ldapEntryConfig) {
StringBuilder filter = new StringBuilder();
for (Condition condition : identityQuery.getConditions()) {
QueryParameter queryParameter = condition.getParameter();
if (!IdentityType.ID.equals(queryParameter)) {
if (AttributeParameter.class.isInstance(queryParameter)) {
AttributeParameter attributeParameter = (AttributeParameter) queryParameter;
String attributeName = ldapEntryConfig.getMappedProperties().get(attributeParameter.getName());
if (attributeName != null) {
if (EqualCondition.class.isInstance(condition)) {
EqualCondition equalCondition = (EqualCondition) condition;
Object parameterValue = equalCondition.getValue();
if (Date.class.isInstance(parameterValue)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(parameterValue).append(")");
} else if (LikeCondition.class.isInstance(condition)) {
LikeCondition likeCondition = (LikeCondition) condition;
String parameterValue = (String) likeCondition.getValue();
} else if (GreaterThanCondition.class.isInstance(condition)) {
GreaterThanCondition greaterThanCondition = (GreaterThanCondition) condition;
Comparable parameterValue = (Comparable) greaterThanCondition.getValue();
if (Date.class.isInstance(parameterValue)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
if (greaterThanCondition.isOrEqual()) {
filter.append("(").append(attributeName).append(">=").append(parameterValue).append(")");
} else {
filter.append("(").append(attributeName).append(">").append(parameterValue).append(")");
}
} else if (LessThanCondition.class.isInstance(condition)) {
LessThanCondition lessThanCondition = (LessThanCondition) condition;
Comparable parameterValue = (Comparable) lessThanCondition.getValue();
if (Date.class.isInstance(parameterValue)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
if (lessThanCondition.isOrEqual()) {
filter.append("(").append(attributeName).append("<=").append(parameterValue).append(")");
} else {
filter.append("(").append(attributeName).append("<").append(parameterValue).append(")");
}
} else if (BetweenCondition.class.isInstance(condition)) {
BetweenCondition betweenCondition = (BetweenCondition) condition;
Comparable x = betweenCondition.getX();
Comparable y = betweenCondition.getY();
if (Date.class.isInstance(x)) {
x = LDAPUtil.formatDate((Date) x);
}
if (Date.class.isInstance(y)) {
y = LDAPUtil.formatDate((Date) y);
}
filter.append("(").append(x).append("<=").append(attributeName).append("<=").append(y).append(")");
} else if (InCondition.class.isInstance(condition)) {
InCondition inCondition = (InCondition) condition;
Object[] valuesToCompare = inCondition.getValue();
filter.append("(&(");
for (int i = 0; i< valuesToCompare.length; i++) {
Object value = valuesToCompare[i];
filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(value).append(")");
}
filter.append("))");
} else {
throw new ModelException("Unsupported query condition [" + condition + "].");
}
}
}
}
}
filter.insert(0, "(&(");
filter.append(getObjectClassesFilter(ldapEntryConfig));
filter.append("))");
return filter;
}
private StringBuilder getObjectClassesFilter(final LDAPMappingConfiguration ldapEntryConfig) {
StringBuilder builder = new StringBuilder();
if (ldapEntryConfig != null && !ldapEntryConfig.getObjectClasses().isEmpty()) {
for (String objectClass : ldapEntryConfig.getObjectClasses()) {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
}
} else {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
}
return builder;
}
private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType) {
return populateAttributedType(searchResult, attributedType, 0);
}
private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType, int hierarchyDepthCount) {
try {
String entryDN = searchResult.getNameInNamespace();
Attributes attributes = searchResult.getAttributes();
if (attributedType == null) {
attributedType = Reflections.newInstance(getConfig().getSupportedTypeByBaseDN(entryDN, getEntryObjectClasses(attributes)));
}
attributedType.setEntryDN(entryDN);
LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
if (hierarchyDepthCount > mappingConfig.getHierarchySearchDepth()) {
return null;
}
if (logger.isTraceEnabled()) {
logger.tracef("Populating attributed type [%s] from DN [%s]", attributedType, entryDN);
}
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
while (ldapAttributes.hasMore()) {
Attribute ldapAttribute = ldapAttributes.next();
Object attributeValue;
try {
attributeValue = ldapAttribute.get();
} catch (NoSuchElementException nsee) {
continue;
}
String ldapAttributeName = ldapAttribute.getID();
if (ldapAttributeName.toLowerCase().equals(getConfig().getUniqueIdentifierAttributeName().toLowerCase())) {
attributedType.setId(this.operationManager.decodeEntryUUID(attributeValue));
} else {
String attributeName = findAttributeName(mappingConfig.getMappedProperties(), ldapAttributeName);
if (attributeName != null) {
// Find if it's java property or attribute
Property<Object> property = PropertyQueries
.createQuery(attributedType.getClass())
.addCriteria(new NamedPropertyCriteria(attributeName)).getFirstResult();
if (property != null) {
if (logger.isTraceEnabled()) {
logger.tracef("Populating property [%s] from ldap attribute [%s] with value [%s] from DN [%s].", property.getName(), ldapAttributeName, attributeValue, entryDN);
}
if (property.getJavaClass().equals(Date.class)) {
property.setValue(attributedType, LDAPUtil.parseDate(attributeValue.toString()));
} else {
property.setValue(attributedType, attributeValue);
}
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Populating attribute [%s] from ldap attribute [%s] with value [%s] from DN [%s].", attributeName, ldapAttributeName, attributeValue, entryDN);
}
attributedType.setAttribute(new org.keycloak.federation.ldap.idm.model.Attribute(attributeName, (Serializable) attributeValue));
}
}
}
}
if (IdentityType.class.isInstance(attributedType)) {
IdentityType identityType = (IdentityType) attributedType;
String createdTimestamp = attributes.get(LDAPConstants.CREATE_TIMESTAMP).get().toString();
identityType.setCreatedDate(LDAPUtil.parseDate(createdTimestamp));
}
LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());
if (mappingConfig.getParentMembershipAttributeName() != null) {
StringBuilder filter = new StringBuilder("(&");
String entryBaseDN = entryDN.substring(entryDN.indexOf(LDAPConstants.COMMA) + 1);
filter
.append("(")
.append(getObjectClassesFilter(entryConfig))
.append(")")
.append("(")
.append(mappingConfig.getParentMembershipAttributeName())
.append(LDAPConstants.EQUAL).append("")
.append(getBindingDN(attributedType, false))
.append(LDAPConstants.COMMA)
.append(entryBaseDN)
.append(")");
filter.append(")");
if (logger.isTraceEnabled()) {
logger.tracef("Searching parent entry for DN [%s] using filter [%s].", entryBaseDN, filter.toString());
}
List<SearchResult> search = this.operationManager.search(getConfig().getBaseDN(), filter.toString(), entryConfig);
if (!search.isEmpty()) {
SearchResult next = search.get(0);
Property<AttributedType> parentProperty = PropertyQueries
.<AttributedType>createQuery(attributedType.getClass())
.addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();
if (parentProperty != null) {
String parentDN = next.getNameInNamespace();
String parentBaseDN = parentDN.substring(parentDN.indexOf(",") + 1);
Class<? extends AttributedType> baseDNType = getConfig().getSupportedTypeByBaseDN(parentBaseDN, getEntryObjectClasses(attributes));
if (parentProperty.getJavaClass().isAssignableFrom(baseDNType)) {
if (logger.isTraceEnabled()) {
logger.tracef("Found parent [%s] for entry for DN [%s].", parentDN, entryDN);
}
int hierarchyDepthCount1 = ++hierarchyDepthCount;
parentProperty.setValue(attributedType, populateAttributedType(next, null, hierarchyDepthCount1));
}
}
} else {
if (logger.isTraceEnabled()) {
logger.tracef("No parent entry found for DN [%s] using filter [%s].", entryDN, filter.toString());
}
}
}
} catch (Exception e) {
throw new ModelException("Could not populate attribute type " + attributedType + ".", e);
}
return attributedType;
}
private String findAttributeName(Map<String, String> attrMapping, String ldapAttributeName) {
for (Map.Entry<String,String> currentAttr : attrMapping.entrySet()) {
if (currentAttr.getValue().equalsIgnoreCase(ldapAttributeName)) {
return currentAttr.getKey();
}
}
return null;
}
private List<String> getEntryObjectClasses(final Attributes attributes) throws NamingException {
Attribute objectClassesAttribute = attributes.get(LDAPConstants.OBJECT_CLASS);
List<String> objectClasses = new ArrayList<String>();
if (objectClassesAttribute == null) {
return objectClasses;
}
NamingEnumeration<?> all = objectClassesAttribute.getAll();
while (all.hasMore()) {
objectClasses.add(all.next().toString());
}
return objectClasses;
}
protected BasicAttributes extractAttributes(AttributedType attributedType, boolean isCreate) {
BasicAttributes entryAttributes = new BasicAttributes();
LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
Map<String, String> mappedProperties = mappingConfig.getMappedProperties();
for (String propertyName : mappedProperties.keySet()) {
if (!mappingConfig.getReadOnlyAttributes().contains(propertyName) && (isCreate || !mappingConfig.getBindingProperty().getName().equals(propertyName))) {
Property<Object> property = PropertyQueries
.<Object>createQuery(attributedType.getClass())
.addCriteria(new NamedPropertyCriteria(propertyName)).getFirstResult();
Object propertyValue = null;
if (property != null) {
// Mapped Java property on the object
propertyValue = property.getValue(attributedType);
} else {
// Not mapped property. So fallback to attribute
org.keycloak.federation.ldap.idm.model.Attribute<?> attribute = attributedType.getAttribute(propertyName);
if (attribute != null) {
propertyValue = attribute.getValue();
}
}
if (AttributedType.class.isInstance(propertyValue)) {
AttributedType referencedType = (AttributedType) propertyValue;
propertyValue = getBindingDN(referencedType, true);
} else {
if (propertyValue == null || isNullOrEmpty(propertyValue.toString())) {
propertyValue = EMPTY_ATTRIBUTE_VALUE;
}
}
entryAttributes.put(mappedProperties.get(propertyName), propertyValue);
}
}
// Don't extract object classes for update
if (isCreate) {
LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(attributedType.getClass());
BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);
for (String objectClassValue : ldapEntryConfig.getObjectClasses()) {
objectClassAttribute.add(objectClassValue);
if (objectClassValue.equals(LDAPConstants.GROUP_OF_NAMES)
|| objectClassValue.equals(LDAPConstants.GROUP_OF_ENTRIES)
|| objectClassValue.equals(LDAPConstants.GROUP_OF_UNIQUE_NAMES)) {
entryAttributes.put(LDAPConstants.MEMBER, EMPTY_ATTRIBUTE_VALUE);
}
}
entryAttributes.put(objectClassAttribute);
}
return entryAttributes;
}
// TODO: Move class StringUtil from SAML module
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
private LDAPMappingConfiguration getMappingConfig(Class<? extends AttributedType> attributedType) {
LDAPMappingConfiguration mappingConfig = getConfig().getMappingConfig(attributedType);
if (mappingConfig == null) {
throw new ModelException("Not mapped type [" + attributedType + "].");
}
return mappingConfig;
}
public String getBindingDN(AttributedType attributedType, boolean appendBaseDN) {
LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
Property<String> idProperty = mappingConfig.getIdProperty();
String baseDN;
if (mappingConfig.getBaseDN() == null || !appendBaseDN) {
baseDN = "";
} else {
baseDN = LDAPConstants.COMMA + getBaseDN(attributedType);
}
Property<String> bindingProperty = mappingConfig.getBindingProperty();
String bindingAttribute;
String dn;
if (bindingProperty == null) {
bindingAttribute = mappingConfig.getMappedProperties().get(idProperty.getName());
dn = idProperty.getValue(attributedType);
} else {
bindingAttribute = mappingConfig.getMappedProperties().get(bindingProperty.getName());
dn = mappingConfig.getBindingProperty().getValue(attributedType);
}
return bindingAttribute + LDAPConstants.EQUAL + dn + baseDN;
}
private String getBaseDN(AttributedType attributedType) {
LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
String baseDN = mappingConfig.getBaseDN();
String parentDN = mappingConfig.getParentMapping().get(mappingConfig.getIdProperty().getValue(attributedType));
if (parentDN != null) {
baseDN = parentDN;
} else {
Property<AttributedType> parentProperty = PropertyQueries
.<AttributedType>createQuery(attributedType.getClass())
.addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();
if (parentProperty != null) {
AttributedType parentType = parentProperty.getValue(attributedType);
if (parentType != null) {
Property<String> parentIdProperty = getMappingConfig(parentType.getClass()).getIdProperty();
String parentId = parentIdProperty.getValue(parentType);
String parentBaseDN = mappingConfig.getParentMapping().get(parentId);
if (parentBaseDN != null) {
baseDN = parentBaseDN;
} else {
baseDN = getBaseDN(parentType);
}
}
}
}
if (baseDN == null) {
baseDN = getConfig().getBaseDN();
}
return baseDN;
}
protected void addToParentAsMember(final AttributedType attributedType) {
LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());
if (entryConfig.getParentMembershipAttributeName() != null) {
Property<AttributedType> parentProperty = PropertyQueries
.<AttributedType>createQuery(attributedType.getClass())
.addCriteria(new TypedPropertyCriteria(attributedType.getClass()))
.getFirstResult();
if (parentProperty != null) {
AttributedType parentType = parentProperty.getValue(attributedType);
if (parentType != null) {
Attributes attributes = this.operationManager.getAttributes(parentType.getId(), getBaseDN(parentType), entryConfig);
Attribute attribute = attributes.get(entryConfig.getParentMembershipAttributeName());
attribute.add(getBindingDN(attributedType, true));
this.operationManager.modifyAttribute(getBindingDN(parentType, true), attribute);
}
}
}
}
protected String getEntryIdentifier(final AttributedType attributedType) {
try {
// we need this to retrieve the entry's identifier from the ldap server
List<SearchResult> search = this.operationManager.search(getBaseDN(attributedType), "(" + getBindingDN(attributedType, false) + ")", getMappingConfig(attributedType.getClass()));
Attribute id = search.get(0).getAttributes().get(getConfig().getUniqueIdentifierAttributeName());
if (id == null) {
throw new ModelException("Could not retrieve identifier for entry [" + getBindingDN(attributedType, true) + "].");
}
return this.operationManager.decodeEntryUUID(id.get());
} catch (NamingException ne) {
throw new ModelException("Could not add type [" + attributedType + "].", ne);
}
}
}

View file

@ -0,0 +1,188 @@
package org.keycloak.federation.ldap.idm.store.ldap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.keycloak.federation.ldap.idm.model.AttributedType;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
/**
* A configuration for the LDAP store.
*
* @author anil saldhana
* @since Sep 6, 2012
*/
public class LDAPIdentityStoreConfiguration {
private String ldapURL;
private String factoryName = "com.sun.jndi.ldap.LdapCtxFactory";
private String authType = "simple";
private String protocol;
private String bindDN;
private String bindCredential;
private boolean activeDirectory;
private Properties connectionProperties;
private boolean pagination;
private String uniqueIdentifierAttributeName;
private boolean userAccountControlsAfterPasswordUpdate;
private String baseDN;
private Map<Class<? extends AttributedType>, LDAPMappingConfiguration> mappingConfig = new HashMap<Class<? extends AttributedType>, LDAPMappingConfiguration>();
public String getLdapURL() {
return this.ldapURL;
}
public String getFactoryName() {
return this.factoryName;
}
public String getAuthType() {
return this.authType;
}
public String getBaseDN() {
return this.baseDN;
}
public String getBindDN() {
return this.bindDN;
}
public String getBindCredential() {
return this.bindCredential;
}
public boolean isActiveDirectory() {
return this.activeDirectory;
}
public Properties getConnectionProperties() {
return this.connectionProperties;
}
public LDAPMappingConfiguration mappingConfig(Class<? extends AttributedType> clazz) {
LDAPMappingConfiguration mappingConfig = new LDAPMappingConfiguration(clazz);
this.mappingConfig.put(clazz, mappingConfig);
return mappingConfig;
}
public Class<? extends AttributedType> getSupportedTypeByBaseDN(String entryDN, List<String> objectClasses) {
String entryBaseDN = entryDN.substring(entryDN.indexOf(LDAPConstants.COMMA) + 1);
for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
if (mappingConfig.getBaseDN() != null) {
if (mappingConfig.getBaseDN().equalsIgnoreCase(entryDN)
|| mappingConfig.getParentMapping().values().contains(entryDN)) {
return mappingConfig.getMappedClass();
}
if (mappingConfig.getBaseDN().equalsIgnoreCase(entryBaseDN)
|| mappingConfig.getParentMapping().values().contains(entryBaseDN)) {
return mappingConfig.getMappedClass();
}
}
}
for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
for (String objectClass : objectClasses) {
if (mappingConfig.getObjectClasses().contains(objectClass)) {
return mappingConfig.getMappedClass();
}
}
}
throw new ModelException("No type found with Base DN [" + entryDN + "] or objectClasses [" + objectClasses + ".");
}
public LDAPMappingConfiguration getMappingConfig(Class<? extends AttributedType> attributedType) {
for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
if (attributedType.equals(mappingConfig.getMappedClass())) {
return mappingConfig;
}
}
return null;
}
public String getProtocol() {
return protocol;
}
public String getUniqueIdentifierAttributeName() {
return uniqueIdentifierAttributeName;
}
public boolean isPagination() {
return pagination;
}
public boolean isUserAccountControlsAfterPasswordUpdate() {
return userAccountControlsAfterPasswordUpdate;
}
public LDAPIdentityStoreConfiguration setLdapURL(String ldapURL) {
this.ldapURL = ldapURL;
return this;
}
public LDAPIdentityStoreConfiguration setFactoryName(String factoryName) {
this.factoryName = factoryName;
return this;
}
public LDAPIdentityStoreConfiguration setAuthType(String authType) {
this.authType = authType;
return this;
}
public LDAPIdentityStoreConfiguration setProtocol(String protocol) {
this.protocol = protocol;
return this;
}
public LDAPIdentityStoreConfiguration setBindDN(String bindDN) {
this.bindDN = bindDN;
return this;
}
public LDAPIdentityStoreConfiguration setBindCredential(String bindCredential) {
this.bindCredential = bindCredential;
return this;
}
public LDAPIdentityStoreConfiguration setActiveDirectory(boolean activeDirectory) {
this.activeDirectory = activeDirectory;
return this;
}
public LDAPIdentityStoreConfiguration setPagination(boolean pagination) {
this.pagination = pagination;
return this;
}
public LDAPIdentityStoreConfiguration setConnectionProperties(Properties connectionProperties) {
this.connectionProperties = connectionProperties;
return this;
}
public LDAPIdentityStoreConfiguration setUniqueIdentifierAttributeName(String uniqueIdentifierAttributeName) {
this.uniqueIdentifierAttributeName = uniqueIdentifierAttributeName;
return this;
}
public LDAPIdentityStoreConfiguration setUserAccountControlsAfterPasswordUpdate(boolean userAccountControlsAfterPasswordUpdate) {
this.userAccountControlsAfterPasswordUpdate = userAccountControlsAfterPasswordUpdate;
return this;
}
public LDAPIdentityStoreConfiguration setBaseDN(String baseDN) {
this.baseDN = baseDN;
return this;
}
}

View file

@ -0,0 +1,231 @@
package org.keycloak.federation.ldap.idm.store.ldap;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.keycloak.federation.ldap.idm.model.Attribute;
import org.keycloak.federation.ldap.idm.model.AttributedType;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.reflection.NamedPropertyCriteria;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyQueries;
/**
* @author pedroigor
*/
public class LDAPMappingConfiguration {
private final Class<? extends AttributedType> mappedClass;
private Set<String> objectClasses;
private String baseDN;
private final Map<String, String> mappedProperties = new HashMap<String, String>();
private Property<String> idProperty;
private Class<? extends AttributedType> relatedAttributedType;
private String parentMembershipAttributeName;
private Map<String, String> parentMapping = new HashMap<String, String>();
private final Set<String> readOnlyAttributes = new HashSet<String>();
private int hierarchySearchDepth;
private Property<String> bindingProperty;
public LDAPMappingConfiguration(Class<? extends AttributedType> mappedClass) {
this.mappedClass = mappedClass;
}
public Class<? extends AttributedType> getMappedClass() {
return this.mappedClass;
}
public Set<String> getObjectClasses() {
return this.objectClasses;
}
public String getBaseDN() {
return this.baseDN;
}
public Map<String, String> getMappedProperties() {
return this.mappedProperties;
}
public Property<String> getIdProperty() {
return this.idProperty;
}
public Property<String> getBindingProperty() {
return this.bindingProperty;
}
public Class<? extends AttributedType> getRelatedAttributedType() {
return this.relatedAttributedType;
}
public String getParentMembershipAttributeName() {
return this.parentMembershipAttributeName;
}
public Map<String, String> getParentMapping() {
return this.parentMapping;
}
public Set<String> getReadOnlyAttributes() {
return this.readOnlyAttributes;
}
public int getHierarchySearchDepth() {
return this.hierarchySearchDepth;
}
private Property getBindingProperty(final String bindingPropertyName) {
Property bindingProperty = PropertyQueries
.<String>createQuery(getMappedClass())
.addCriteria(new NamedPropertyCriteria(bindingPropertyName)).getFirstResult();
// We don't have Java property, so actually delegate to setAttribute/getAttribute
if (bindingProperty == null) {
bindingProperty = new Property<String>() {
@Override
public String getName() {
return bindingPropertyName;
}
@Override
public Type getBaseType() {
return null;
}
@Override
public Class<String> getJavaClass() {
return String.class;
}
@Override
public AnnotatedElement getAnnotatedElement() {
return null;
}
@Override
public Member getMember() {
return null;
}
@Override
public String getValue(Object instance) {
if (!(instance instanceof AttributedType)) {
throw new IllegalStateException("Instance [ " + instance + " ] not an instance of AttributedType");
}
AttributedType attributedType = (AttributedType) instance;
Attribute<String> attr = attributedType.getAttribute(bindingPropertyName);
return attr!=null ? attr.getValue() : null;
}
@Override
public void setValue(Object instance, String value) {
if (!(instance instanceof AttributedType)) {
throw new IllegalStateException("Instance [ " + instance + " ] not an instance of AttributedType");
}
AttributedType attributedType = (AttributedType) instance;
attributedType.setAttribute(new Attribute(bindingPropertyName, value));
}
@Override
public Class<?> getDeclaringClass() {
return null;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public void setAccessible() {
}
@Override
public boolean isAnnotationPresent(Class annotation) {
return false;
}
};
}
return bindingProperty;
}
public LDAPMappingConfiguration setObjectClasses(Set<String> objectClasses) {
this.objectClasses = objectClasses;
return this;
}
public LDAPMappingConfiguration setBaseDN(String baseDN) {
this.baseDN = baseDN;
return this;
}
public LDAPMappingConfiguration addAttributeMapping(String userAttributeName, String ldapAttributeName) {
this.mappedProperties.put(userAttributeName, ldapAttributeName);
return this;
}
public LDAPMappingConfiguration addReadOnlyAttributeMapping(String userAttributeName, String ldapAttributeName) {
this.mappedProperties.put(userAttributeName, ldapAttributeName);
this.readOnlyAttributes.add(userAttributeName);
return this;
}
public LDAPMappingConfiguration setIdPropertyName(String idPropertyName) {
if (idPropertyName != null) {
this.idProperty = PropertyQueries
.<String>createQuery(getMappedClass())
.addCriteria(new NamedPropertyCriteria(idPropertyName)).getFirstResult();
} else {
this.idProperty = null;
}
if (IdentityType.class.isAssignableFrom(mappedClass) && idProperty == null) {
throw new ModelException("Id attribute not mapped to any property of [" + mappedClass + "].");
}
// Binding property is idProperty by default
if (this.bindingProperty == null) {
this.bindingProperty = this.idProperty;
}
return this;
}
public LDAPMappingConfiguration setRelatedAttributedType(Class<? extends AttributedType> relatedAttributedType) {
this.relatedAttributedType = relatedAttributedType;
return this;
}
public LDAPMappingConfiguration setParentMembershipAttributeName(String parentMembershipAttributeName) {
this.parentMembershipAttributeName = parentMembershipAttributeName;
return this;
}
public LDAPMappingConfiguration setParentMapping(Map<String, String> parentMapping) {
this.parentMapping = parentMapping;
return this;
}
public LDAPMappingConfiguration setHierarchySearchDepth(int hierarchySearchDepth) {
this.hierarchySearchDepth = hierarchySearchDepth;
return this;
}
public LDAPMappingConfiguration setBindingPropertyName(String bindingPropertyName) {
this.bindingProperty = getBindingProperty(bindingPropertyName);
return this;
}
}

View file

@ -0,0 +1,606 @@
package org.keycloak.federation.ldap.idm.store.ldap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import static javax.naming.directory.SearchControls.SUBTREE_SCOPE;
/**
* <p>This class provides a set of operations to manage LDAP trees.</p>
*
* @author Anil Saldhana
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
*/
public class LDAPOperationManager {
private static final Logger logger = Logger.getLogger(LDAPOperationManager.class);
private final LDAPIdentityStoreConfiguration config;
private final Map<String, Object> connectionProperties;
public LDAPOperationManager(LDAPIdentityStoreConfiguration config) throws NamingException {
this.config = config;
this.connectionProperties = Collections.unmodifiableMap(createConnectionProperties());
}
/**
* <p>
* Modifies the given {@link javax.naming.directory.Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attribute
*/
public void modifyAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Modifies the given {@link Attribute} instances using the given DN. This method performs a REPLACE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attributes
*/
public void modifyAttributes(String dn, NamingEnumeration<Attribute> attributes) {
try {
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
while (attributes.hasMore()) {
ModificationItem modItem = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attributes.next());
modItems.add(modItem);
}
modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}));
} catch (NamingException ne) {
throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", ne);
}
}
/**
* <p>
* Removes the given {@link Attribute} instance using the given DN. This method performs a REMOVE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attribute
*/
public void removeAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Adds the given {@link Attribute} instance using the given DN. This method performs a ADD_ATTRIBUTE operation.
* </p>
*
* @param dn
* @param attribute
*/
public void addAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Searches the LDAP tree.
* </p>
*
* @param baseDN
* @param id
*
* @return
*/
public void removeEntryById(final String baseDN, final String id, final LDAPMappingConfiguration mappingConfiguration) {
final String filter = getFilterById(baseDN, id);
try {
final SearchControls cons = getSearchControls(mappingConfiguration);
execute(new LdapOperation<SearchResult>() {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> result = context.search(baseDN, filter, cons);
if (result.hasMore()) {
SearchResult sr = result.next();
if (logger.isDebugEnabled()) {
logger.debugf("Removing entry [%s] with attributes: [", sr.getNameInNamespace());
NamingEnumeration<? extends Attribute> all = sr.getAttributes().getAll();
while (all.hasMore()) {
Attribute attribute = all.next();
logger.debugf(" %s = %s", attribute.getID(), attribute.get());
}
logger.debugf("]");
}
destroySubcontext(context, sr.getNameInNamespace());
}
result.close();
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not remove entry from DN [" + baseDN + "] and id [" + id + "]", e);
}
}
public List<SearchResult> search(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration) throws NamingException {
final List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(mappingConfiguration);
try {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
}
search.close();
return result;
}
});
} catch (NamingException e) {
logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
throw e;
}
}
public <V extends IdentityType> List<SearchResult> searchPaginated(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration, final IdentityQuery<V> identityQuery) throws NamingException {
final List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(mappingConfiguration);
try {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
try {
byte[] cookie = (byte[])identityQuery.getPaginationContext();
PagedResultsControl pagedControls = new PagedResultsControl(identityQuery.getLimit(), cookie, Control.CRITICAL);
context.setRequestControls(new Control[] { pagedControls });
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
}
search.close();
Control[] responseControls = context.getResponseControls();
if (responseControls != null) {
for (Control respControl : responseControls) {
if (respControl instanceof PagedResultsResponseControl) {
PagedResultsResponseControl prrc = (PagedResultsResponseControl)respControl;
cookie = prrc.getCookie();
identityQuery.setPaginationContext(cookie);
}
}
}
return result;
} catch (IOException ioe) {
logger.errorf(ioe, "Could not query server with paginated query using DN [%s], filter [%s]", baseDN, filter);
throw new NamingException(ioe.getMessage());
}
}
});
} catch (NamingException e) {
logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
throw e;
}
}
private SearchControls getSearchControls(LDAPMappingConfiguration mappingConfiguration) {
final SearchControls cons = new SearchControls();
cons.setSearchScope(SUBTREE_SCOPE);
cons.setReturningObjFlag(false);
List<String> returningAttributes = getReturningAttributes(mappingConfiguration);
cons.setReturningAttributes(returningAttributes.toArray(new String[returningAttributes.size()]));
return cons;
}
public String getFilterById(String baseDN, String id) {
String filter = null;
if (this.config.isActiveDirectory()) {
final String strObjectGUID = "<GUID=" + id + ">";
try {
Attributes attributes = execute(new LdapOperation<Attributes>() {
@Override
public Attributes execute(LdapContext context) throws NamingException {
return context.getAttributes(strObjectGUID);
}
});
byte[] objectGUID = (byte[]) attributes.get(LDAPConstants.OBJECT_GUID).get();
filter = "(&(objectClass=*)(" + getUniqueIdentifierAttributeName() + LDAPConstants.EQUAL + LDAPUtil.convertObjectGUIToByteString(objectGUID) + "))";
} catch (NamingException ne) {
return filter;
}
}
if (filter == null) {
filter = "(&(objectClass=*)(" + getUniqueIdentifierAttributeName() + LDAPConstants.EQUAL + id + "))";
}
return filter;
}
public SearchResult lookupById(final String baseDN, final String id, final LDAPMappingConfiguration mappingConfiguration) {
final String filter = getFilterById(baseDN, id);
try {
final SearchControls cons = getSearchControls(mappingConfiguration);
return execute(new LdapOperation<SearchResult>() {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
try {
if (search.hasMoreElements()) {
return search.next();
}
} finally {
if (search != null) {
search.close();
}
}
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not query server using DN [" + baseDN + "] and filter [" + filter + "]", e);
}
}
/**
* <p>
* Destroys a subcontext with the given DN from the LDAP tree.
* </p>
*
* @param dn
*/
private void destroySubcontext(LdapContext context, final String dn) {
try {
NamingEnumeration<Binding> enumeration = null;
try {
enumeration = context.listBindings(dn);
while (enumeration.hasMore()) {
Binding binding = enumeration.next();
String name = binding.getNameInNamespace();
destroySubcontext(context, name);
}
context.unbind(dn);
} finally {
try {
enumeration.close();
} catch (Exception e) {
}
}
} catch (Exception e) {
throw new ModelException("Could not unbind DN [" + dn + "]", e);
}
}
/**
* <p>
* Performs a simple authentication using the given DN and password to bind to the authentication context.
* </p>
*
* @param dn
* @param password
*
* @return
*/
public boolean authenticate(String dn, String password) {
InitialContext authCtx = null;
try {
Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, password);
// Never use connection pool to prevent password caching
env.put("com.sun.jndi.ldap.connect.pool", "false");
authCtx = new InitialLdapContext(env, null);
return true;
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debugf(e, "Authentication failed for DN [%s]", dn);
}
return false;
} finally {
if (authCtx != null) {
try {
authCtx.close();
} catch (NamingException e) {
}
}
}
}
public void modifyAttributes(final String dn, final ModificationItem[] mods) {
try {
if (logger.isDebugEnabled()) {
logger.debugf("Modifying attributes for entry [%s]: [", dn);
for (ModificationItem item : mods) {
Object values;
if (item.getAttribute().size() > 0) {
values = item.getAttribute().get();
} else {
values = "No values";
}
logger.debugf(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
}
logger.debugf("]");
}
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
context.modifyAttributes(dn, mods);
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
}
}
public void createSubContext(final String name, final Attributes attributes) {
try {
if (logger.isDebugEnabled()) {
logger.debugf("Creating entry [%s] with attributes: [", name);
NamingEnumeration<? extends Attribute> all = attributes.getAll();
while (all.hasMore()) {
Attribute attribute = all.next();
logger.debugf(" %s = %s", attribute.getID(), attribute.get());
}
logger.debugf("]");
}
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
DirContext subcontext = context.createSubcontext(name, attributes);
subcontext.close();
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Error creating subcontext [" + name + "]", e);
}
}
private String getUniqueIdentifierAttributeName() {
return this.config.getUniqueIdentifierAttributeName();
}
private NamingEnumeration<SearchResult> createEmptyEnumeration() {
return new NamingEnumeration<SearchResult>() {
@Override
public SearchResult next() throws NamingException {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public boolean hasMore() throws NamingException {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void close() throws NamingException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public boolean hasMoreElements() {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public SearchResult nextElement() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
};
}
public Attributes getAttributes(final String entryUUID, final String baseDN, LDAPMappingConfiguration mappingConfiguration) {
SearchResult search = lookupById(baseDN, entryUUID, mappingConfiguration);
if (search == null) {
throw new ModelException("Couldn't find item with entryUUID [" + entryUUID + "] and baseDN [" + baseDN + "]");
}
return search.getAttributes();
}
public String decodeEntryUUID(final Object entryUUID) {
String id;
if (this.config.isActiveDirectory()) {
id = LDAPUtil.decodeObjectGUID((byte[]) entryUUID);
} else {
id = entryUUID.toString();
}
return id;
}
private LdapContext createLdapContext() throws NamingException {
return new InitialLdapContext(new Hashtable<Object, Object>(this.connectionProperties), null);
}
private Map<String, Object> createConnectionProperties() {
HashMap<String, Object> env = new HashMap<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, this.config.getFactoryName());
env.put(Context.SECURITY_AUTHENTICATION, this.config.getAuthType());
String protocol = this.config.getProtocol();
if (protocol != null) {
env.put(Context.SECURITY_PROTOCOL, protocol);
}
String bindDN = this.config.getBindDN();
char[] bindCredential = null;
if (this.config.getBindCredential() != null) {
bindCredential = this.config.getBindCredential().toCharArray();
}
if (bindDN != null) {
env.put(Context.SECURITY_PRINCIPAL, bindDN);
env.put(Context.SECURITY_CREDENTIALS, bindCredential);
}
String url = this.config.getLdapURL();
if (url == null) {
throw new RuntimeException("url");
}
env.put(Context.PROVIDER_URL, url);
// Just dump the additional properties
Properties additionalProperties = this.config.getConnectionProperties();
if (additionalProperties != null) {
for (Object key : additionalProperties.keySet()) {
env.put(key.toString(), additionalProperties.getProperty(key.toString()));
}
}
if (config.isActiveDirectory()) {
env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
}
if (logger.isDebugEnabled()) {
logger.debugf("Creating LdapContext using properties: [%s]", env);
}
return env;
}
private <R> R execute(LdapOperation<R> operation) throws NamingException {
LdapContext context = null;
try {
context = createLdapContext();
return operation.execute(context);
} catch (NamingException ne) {
logger.error("Could not create Ldap context or operation execution error.", ne);
throw ne;
} finally {
if (context != null) {
try {
context.close();
} catch (NamingException ne) {
logger.error("Could not close Ldap context.", ne);
}
}
}
}
private interface LdapOperation<R> {
R execute(LdapContext context) throws NamingException;
}
private List<String> getReturningAttributes(final LDAPMappingConfiguration mappingConfiguration) {
List<String> returningAttributes = new ArrayList<String>();
if (mappingConfiguration != null) {
returningAttributes.addAll(mappingConfiguration.getMappedProperties().values());
returningAttributes.add(mappingConfiguration.getParentMembershipAttributeName());
// for (LDAPMappingConfiguration relationshipConfig : this.config.getRelationshipConfigs()) {
// if (relationshipConfig.getRelatedAttributedType().equals(mappingConfiguration.getMappedClass())) {
// returningAttributes.addAll(relationshipConfig.getMappedProperties().values());
// }
// }
} else {
returningAttributes.add("*");
}
returningAttributes.add(getUniqueIdentifierAttributeName());
returningAttributes.add(LDAPConstants.CREATE_TIMESTAMP);
returningAttributes.add(LDAPConstants.OBJECT_CLASS);
return returningAttributes;
}
}

View file

@ -0,0 +1,158 @@
package org.keycloak.federation.ldap.idm.store.ldap;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import org.keycloak.models.ModelException;
/**
* <p>Utility class for working with LDAP.</p>
*
* @author Pedro Igor
*/
public class LDAPUtil {
/**
* <p>Formats the given date.</p>
*
* @param date The Date to format.
*
* @return A String representing the formatted date.
*/
public static final String formatDate(Date date) {
if (date == null) {
throw new IllegalArgumentException("You must provide a date.");
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'.0Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(date);
}
/**
* <p>
* Parses dates/time stamps stored in LDAP. Some possible values:
* </p>
* <ul>
* <li>20020228150820</li>
* <li>20030228150820Z</li>
* <li>20050228150820.12</li>
* <li>20060711011740.0Z</li>
* </ul>
*
* @param date The date string to parse from.
*
* @return the Date.
*/
public static final Date parseDate(String date) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
try {
if (date.endsWith("Z")) {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
} else {
dateFormat.setTimeZone(TimeZone.getDefault());
}
return dateFormat.parse(date);
} catch (Exception e) {
throw new ModelException("Error converting ldap date.", e);
}
}
/**
* <p>Creates a byte-based {@link String} representation of a raw byte array representing the value of the
* <code>objectGUID</code> attribute retrieved from Active Directory.</p>
*
* <p>The returned string is useful to perform queries on AD based on the <code>objectGUID</code> value. Eg.:</p>
*
* <p>
* String filter = "(&(objectClass=*)(objectGUID" + EQUAL + convertObjectGUIToByteString(objectGUID) + "))";
* </p>
*
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
* Active Directory.
*
* @return A byte-based String representation in the form of \[0]\[1]\[2]\[3]\[4]\[5]\[6]\[7]\[8]\[9]\[10]\[11]\[12]\[13]\[14]\[15]
*/
public static String convertObjectGUIToByteString(byte[] objectGUID) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < objectGUID.length; i++) {
String transformed = prefixZeros((int) objectGUID[i] & 0xFF);
result.append("\\");
result.append(transformed);
}
return result.toString();
}
/**
* <p>Decode a raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from Active
* Directory.</p>
*
* <p>The returned string is useful to directly bind an entry. Eg.:</p>
*
* <p>
* String bindingString = decodeObjectGUID(objectGUID);
* <br/>
* Attributes attributes = ctx.getAttributes(bindingString);
* </p>
*
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
* Active Directory.
*
* @return A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15].
*/
public static String decodeObjectGUID(byte[] objectGUID) {
StringBuilder displayStr = new StringBuilder();
displayStr.append(convertToDashedString(objectGUID));
return displayStr.toString();
}
private static String convertToDashedString(byte[] objectGUID) {
StringBuilder displayStr = new StringBuilder();
displayStr.append(prefixZeros((int) objectGUID[3] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[2] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[1] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[0] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[5] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[4] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[7] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[6] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[8] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[9] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[10] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[11] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[12] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[13] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[14] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[15] & 0xFF));
return displayStr.toString();
}
private static String prefixZeros(int value) {
if (value <= 0xF) {
StringBuilder sb = new StringBuilder("0");
sb.append(Integer.toHexString(value));
return sb.toString();
} else {
return Integer.toHexString(value);
}
}
}

View file

@ -29,5 +29,40 @@ public class LDAPConstants {
public static final String BATCH_SIZE_FOR_SYNC = "batchSizeForSync";
public static final int DEFAULT_BATCH_SIZE_FOR_SYNC = 1000;
// Config option to specify if registrations will be synced or not
public static final String SYNC_REGISTRATIONS = "syncRegistrations";
// Applicable just for active directory
public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate";
// Custom attributes on UserModel, which is mapped to LDAP
public static final String LDAP_ID = "LDAP_ID";
public static final String LDAP_ENTRY_DN = "LDAP_ENTRY_DN";
// Those are forked from Picketlink
public static final String GIVENNAME = "givenname";
public static final String CN = "cn";
public static final String SN = "sn";
public static final String EMAIL = "mail";
public static final String MEMBER = "member";
public static final String MEMBER_OF = "memberOf";
public static final String OBJECT_CLASS = "objectclass";
public static final String UID = "uid";
public static final String USER_PASSWORD_ATTRIBUTE = "userpassword";
public static final String GROUP_OF_NAMES = "groupOfNames";
public static final String GROUP_OF_ENTRIES = "groupOfEntries";
public static final String GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames";
public static final String COMMA = ",";
public static final String EQUAL = "=";
public static final String SPACE_STRING = " ";
public static final String CUSTOM_ATTRIBUTE_ENABLED = "enabled";
public static final String CUSTOM_ATTRIBUTE_CREATE_DATE = "createDate";
public static final String CUSTOM_ATTRIBUTE_EXPIRY_DATE = "expiryDate";
public static final String ENTRY_UUID = "entryUUID";
public static final String OBJECT_GUID = "objectGUID";
public static final String CREATE_TIMESTAMP = "createTimeStamp";
public static final String MODIFY_TIMESTAMP = "modifyTimeStamp";
}

View file

@ -0,0 +1,40 @@
package org.keycloak.models.utils.reflection;
import java.beans.Introspector;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* A criteria that matches a property based on name
*
* @see PropertyCriteria
*/
public class NamedPropertyCriteria implements PropertyCriteria {
private final String[] propertyNames;
public NamedPropertyCriteria(String... propertyNames) {
this.propertyNames = propertyNames;
}
public boolean fieldMatches(Field f) {
for (String propertyName : propertyNames) {
if (propertyName.equals(f.getName())) {
return true;
}
}
return false;
}
public boolean methodMatches(Method m) {
String[] validPrefix = {"get", "is"};
for (String propertyName : propertyNames) {
for (String prefix : validPrefix) {
if (m.getName().startsWith(prefix) &&
Introspector.decapitalize(m.getName().substring(prefix.length())).equals(propertyName)) {
return true;
}
}
}
return false;
}
}

View file

@ -0,0 +1,71 @@
package org.keycloak.models.utils.reflection;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* A criteria that matches a property based on its type
*
* @see PropertyCriteria
*/
public class TypedPropertyCriteria implements PropertyCriteria {
/**
* <p> Different options can be used to match a specific property based on its type. Regardless of the option
* chosen, if the property type equals the <code>propertyClass</code> it will be selected. <p/> <ul> <li>SUB_TYPE:
* Also consider properties where its type is a subtype of <code>propertyClass</code>. .</li> <li>SUPER_TYPE: Also
* consider properties where its type is a superclass or superinterface of <code>propertyClass</code>. .</li> </ul>
* </p>
*/
public static enum MatchOption {
SUB_TYPE, SUPER_TYPE, ALL
}
private final Class<?> propertyClass;
private final MatchOption matchOption;
public TypedPropertyCriteria(Class<?> propertyClass) {
this(propertyClass, null);
}
public TypedPropertyCriteria(Class<?> propertyClass, MatchOption matchOption) {
if (propertyClass == null) {
throw new IllegalArgumentException("Property class can not be null.");
}
this.propertyClass = propertyClass;
this.matchOption = matchOption;
}
public boolean fieldMatches(Field f) {
return match(f.getType());
}
public boolean methodMatches(Method m) {
return match(m.getReturnType());
}
private boolean match(Class<?> type) {
if (propertyClass.equals(type)) {
return true;
} else {
boolean matchSubType = propertyClass.isAssignableFrom(type);
if (MatchOption.SUB_TYPE == this.matchOption) {
return matchSubType;
}
boolean matchSuperType = type.isAssignableFrom(propertyClass);
if (MatchOption.SUPER_TYPE == this.matchOption) {
return matchSuperType;
}
if (MatchOption.ALL == this.matchOption) {
return matchSubType || matchSuperType;
}
}
return false;
}
}

View file

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-picketlink-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-picketlink-api</artifactId>
<name>Keycloak Picketlink API</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
org.keycloak.picketlink.PartitionManagerSpi

View file

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-picketlink-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-picketlink-ldap</artifactId>
<name>Keycloak Picketlink LDAP</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<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.picketlink</groupId>
<artifactId>picketlink-idm-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-impl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -1,63 +0,0 @@
package org.keycloak.picketlink.idm;
import org.jboss.logging.Logger;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.event.CredentialUpdatedEvent;
import org.picketlink.idm.event.EventBridge;
import org.picketlink.idm.internal.ContextualIdentityManager;
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;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
/**
* @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;
if (updateUserAccountAfterPasswordUpdate) {
logger.info("userAccountControl attribute will be updated in Active Directory after user registration");
}
}
@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();
ContextualIdentityManager identityManager = (ContextualIdentityManager) partitionManager.createIdentityManager();
IdentityContext identityCtx = identityManager.getIdentityContext();
CredentialStore store = identityManager.getStoreSelector().getStoreForCredentialOperation(identityCtx, credEvent.getCredential().getClass());
if (store instanceof LDAPIdentityStore) {
LDAPIdentityStore ldapStore = (LDAPIdentityStore)store;
LDAPOperationManager operationManager = ldapStore.getOperationManager();
User picketlinkUser = (User) credEvent.getAccount();
String userDN = ldapStore.getBindingDN(picketlinkUser, true);
ModificationItem[] mods = new ModificationItem[1];
BasicAttribute mod0 = new BasicAttribute("userAccountControl", "512");
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttribute(userDN, mod0);
logger.debug("Attribute userAccountControls switched to 512 after password update of user " + picketlinkUser.getLoginName());
} else {
logger.debug("Store for credential updates is not LDAPIdentityStore. Ignored");
}
}
}
}

View file

@ -1,51 +0,0 @@
package org.keycloak.picketlink.idm;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.config.LDAPMappingConfiguration;
import org.picketlink.idm.credential.UsernamePasswordCredentials;
import org.picketlink.idm.credential.storage.CredentialStorage;
import org.picketlink.idm.ldap.internal.LDAPIdentityStore;
import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler;
import org.picketlink.idm.model.Account;
import org.picketlink.idm.model.basic.BasicModel;
import org.picketlink.idm.model.basic.User;
import org.picketlink.idm.spi.IdentityContext;
import javax.naming.directory.SearchResult;
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);
}
@Override
protected boolean validateCredential(IdentityContext context, CredentialStorage credentialStorage, UsernamePasswordCredentials credentials, LDAPIdentityStore ldapIdentityStore) {
Account account = getAccount(context, credentials.getUsername());
char[] password = credentials.getPassword().getValue();
String userDN = (String) account.getAttribute(LDAPIdentityStore.ENTRY_DN_ATTRIBUTE_NAME).getValue();
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
CREDENTIAL_LOGGER.debugf("Using DN [%s] for authentication of user [%s]", userDN, credentials.getUsername());
}
if (ldapIdentityStore.getOperationManager().authenticate(userDN, new String(password))) {
return true;
}
return false;
}
}

View file

@ -1,26 +0,0 @@
package org.keycloak.picketlink.ldap;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.picketlink.PartitionManagerProvider;
import org.picketlink.idm.PartitionManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPPartitionManagerProvider implements PartitionManagerProvider {
private final PartitionManagerRegistry partitionManagerRegistry;
public LDAPPartitionManagerProvider(PartitionManagerRegistry partitionManagerRegistry) {
this.partitionManagerRegistry = partitionManagerRegistry;
}
@Override
public PartitionManager getPartitionManager(UserFederationProviderModel model) {
return partitionManagerRegistry.getPartitionManager(model);
}
@Override
public void close() {
}
}

View file

@ -1,43 +0,0 @@
package org.keycloak.picketlink.ldap;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
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 UserFederationModel configuration for it
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPPartitionManagerProviderFactory implements PartitionManagerProviderFactory {
private PartitionManagerRegistry partitionManagerRegistry;
@Override
public PartitionManagerProvider create(KeycloakSession session) {
return new LDAPPartitionManagerProvider(partitionManagerRegistry);
}
@Override
public void init(Config.Scope config) {
partitionManagerRegistry = new PartitionManagerRegistry();
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "ldap";
}
}

View file

@ -1,163 +0,0 @@
package org.keycloak.picketlink.ldap;
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 java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import static org.picketlink.common.constants.LDAPConstants.*;
/**
* @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(UserFederationProviderModel model) {
PartitionManagerContext context = partitionManagers.get(model.getId());
// 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)) {
logLDAPConfig(model.getId(), config);
PartitionManager manager = createPartitionManager(config);
context = new PartitionManagerContext(config, manager);
partitionManagers.put(model.getId(), context);
}
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
*/
public static PartitionManager createPartitionManager(Map<String,String> ldapConfig) {
IdentityConfigurationBuilder builder = new IdentityConfigurationBuilder();
Properties connectionProps = new Properties();
if (ldapConfig.containsKey(LDAPConstants.CONNECTION_POOLING)) {
connectionProps.put("com.sun.jndi.ldap.connect.pool", ldapConfig.get(LDAPConstants.CONNECTION_POOLING));
}
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", "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");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
String vendor = ldapConfig.get(LDAPConstants.VENDOR);
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;
}
String ldapFirstNameMapping = activeDirectory ? "givenName" : CN;
String createTimestampMapping = activeDirectory ? "whenCreated" : CREATE_TIMESTAMP;
String modifyTimestampMapping = activeDirectory ? "whenChanged" : MODIFY_TIMESTAMP;
String[] userObjectClasses = getUserObjectClasses(ldapConfig);
boolean pagination = ldapConfig.containsKey(LDAPConstants.PAGINATION) ? Boolean.parseBoolean(ldapConfig.get(LDAPConstants.PAGINATION)) : false;
// Use same mapping for User and Agent for now
LDAPStoreConfigurationBuilder ldapStoreBuilder =
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()
.pagination(pagination);
// RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID"
if (vendor != null && vendor.equals(LDAPConstants.VENDOR_RHDS)) {
ldapStoreBuilder.uniqueIdentifierAttributeName("nsuniqueid");
} else if (LDAPConstants.VENDOR_TIVOLI.equals(vendor)) {
ldapStoreBuilder.uniqueIdentifierAttributeName("uniqueidentifier");
}
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)
.readOnlyAttribute("createdDate", createTimestampMapping)
.readOnlyAttribute("modifyDate", modifyTimestampMapping);
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) {
if (System.getProperty(name) == null) {
System.setProperty(name, defaultValue);
}
}
// 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);
String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson, organizationalPerson";
String[] objectClasses = objClassesStr.split(",");
// Trim them
String[] userObjectClasses = new String[objectClasses.length];
for (int i=0 ; i<objectClasses.length ; i++) {
userObjectClasses[i] = objectClasses[i].trim();
}
return userObjectClasses;
}
private class PartitionManagerContext {
private PartitionManagerContext(Map<String,String> config, PartitionManager manager) {
this.config = config;
this.partitionManager = manager;
}
private Map<String,String> config;
private PartitionManager partitionManager;
}
}

View file

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

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>keycloak-picketlink-parent</artifactId>
<name>Keycloak Picketlink</name>
<description />
<modules>
<module>keycloak-picketlink-api</module>
<module>keycloak-picketlink-ldap</module>
</modules>
</project>

View file

@ -114,7 +114,6 @@
<module>model</module>
<module>integration</module>
<module>proxy</module>
<module>picketlink</module>
<module>federation</module>
<module>services</module>
<module>saml</module>

View file

@ -102,12 +102,6 @@
<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.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>

View file

@ -12,6 +12,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.LDAPUtils;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelReadOnlyException;
@ -21,7 +23,6 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.picketlink.PartitionManagerProvider;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.OAuthClient;
@ -35,8 +36,6 @@ import org.keycloak.testsuite.rule.LDAPRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.basic.User;
import java.util.Map;
@ -57,19 +56,19 @@ public class FederationProvidersIntegrationTest {
addUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
Map<String,String> ldapConfig = ldapRule.getConfig();
ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "true");
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
// Delete all LDAP users and add some new for testing
PartitionManager partitionManager = getPartitionManager(manager.getSession(), ldapModel);
LDAPUtils.removeAllUsers(partitionManager);
LDAPIdentityStore ldapStore = getLdapIdentityStore(manager.getSession(), ldapModel);
LDAPUtils.removeAllUsers(ldapStore);
User john = LDAPUtils.addUser(partitionManager, "johnkeycloak", "John", "Doe", "john@email.org");
LDAPUtils.updatePassword(partitionManager, john, "Password1");
LDAPUser john = LDAPUtils.addUser(ldapStore, "johnkeycloak", "John", "Doe", "john@email.org");
LDAPUtils.updatePassword(ldapStore, john, "Password1");
User existing = LDAPUtils.addUser(partitionManager, "existing", "Existing", "Foo", "existing@email.org");
LDAPUser existing = LDAPUtils.addUser(ldapStore, "existing", "Existing", "Foo", "existing@email.org");
}
});
@ -339,13 +338,13 @@ public class FederationProvidersIntegrationTest {
@Test
public void testSearch() {
KeycloakSession session = keycloakRule.startSession();
PartitionManager partitionManager = getPartitionManager(session, ldapModel);
LDAPIdentityStore ldapStore = getLdapIdentityStore(session, ldapModel);
try {
RealmModel appRealm = session.realms().getRealmByName("test");
LDAPUtils.addUser(partitionManager, "username1", "John1", "Doel1", "user1@email.org");
LDAPUtils.addUser(partitionManager, "username2", "John2", "Doel2", "user2@email.org");
LDAPUtils.addUser(partitionManager, "username3", "John3", "Doel3", "user3@email.org");
LDAPUtils.addUser(partitionManager, "username4", "John4", "Doel4", "user4@email.org");
LDAPUtils.addUser(ldapStore, "username1", "John1", "Doel1", "user1@email.org");
LDAPUtils.addUser(ldapStore, "username2", "John2", "Doel2", "user2@email.org");
LDAPUtils.addUser(ldapStore, "username3", "John3", "Doel3", "user3@email.org");
LDAPUtils.addUser(ldapStore, "username4", "John4", "Doel4", "user4@email.org");
// Users are not at local store at this moment
Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm));
@ -395,7 +394,7 @@ public class FederationProvidersIntegrationTest {
Assert.assertTrue(session.users().validCredentials(appRealm, user, cred));
// LDAP password is still unchanged
Assert.assertTrue(LDAPUtils.validatePassword(getPartitionManager(session, model), "johnkeycloak", "Password1"));
Assert.assertTrue(LDAPUtils.validatePassword(getLdapIdentityStore(session, model), user, "Password1"));
// ATM it's not permitted to delete user in unsynced mode. Should be user deleted just locally instead?
Assert.assertFalse(session.users().removeUser(appRealm, user));
@ -412,9 +411,10 @@ public class FederationProvidersIntegrationTest {
}
}
static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
PartitionManagerProvider partitionManagerProvider = keycloakSession.getProvider(PartitionManagerProvider.class);
return partitionManagerProvider.getPartitionManager(ldapFedModel);
static LDAPIdentityStore getLdapIdentityStore(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
LDAPFederationProviderFactory ldapProviderFactory = (LDAPFederationProviderFactory) keycloakSession.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, ldapFedModel.getProviderName());
LDAPFederationProvider ldapProvider = ldapProviderFactory.getInstance(keycloakSession, ldapFedModel);
return ldapProvider.getLdapIdentityStore();
}
}

View file

@ -7,9 +7,10 @@ import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.LDAPUtils;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.LDAPConstants;
@ -25,8 +26,6 @@ import org.keycloak.testsuite.rule.LDAPRule;
import org.keycloak.testutils.DummyUserFederationProviderFactory;
import org.keycloak.timer.TimerProvider;
import org.keycloak.util.Time;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.basic.User;
import java.util.HashMap;
import java.util.Map;
@ -50,26 +49,20 @@ public class SyncProvidersTest {
Time.setOffset(0);
Map<String,String> ldapConfig = ldapRule.getConfig();
ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "false");
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "false");
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap",
-1, -1, 0);
// Delete all LDAP users and add 5 new users for testing
PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(manager.getSession(), ldapModel);
LDAPUtils.removeAllUsers(partitionManager);
LDAPIdentityStore ldapStore = FederationProvidersIntegrationTest.getLdapIdentityStore(manager.getSession(), ldapModel);
LDAPUtils.removeAllUsers(ldapStore);
User user1 = LDAPUtils.addUser(partitionManager, "user1", "User1FN", "User1LN", "user1@email.org");
LDAPUtils.updatePassword(partitionManager, user1, "Password1");
User user2 = LDAPUtils.addUser(partitionManager, "user2", "User2FN", "User2LN", "user2@email.org");
LDAPUtils.updatePassword(partitionManager, user2, "Password2");
User user3 = LDAPUtils.addUser(partitionManager, "user3", "User3FN", "User3LN", "user3@email.org");
LDAPUtils.updatePassword(partitionManager, user3, "Password3");
User user4 = LDAPUtils.addUser(partitionManager, "user4", "User4FN", "User4LN", "user4@email.org");
LDAPUtils.updatePassword(partitionManager, user4, "Password4");
User user5 = LDAPUtils.addUser(partitionManager, "user5", "User5FN", "User5LN", "user5@email.org");
LDAPUtils.updatePassword(partitionManager, user5, "Password5");
for (int i=1 ; i<6 ; i++) {
LDAPUser user = LDAPUtils.addUser(ldapStore, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org");
LDAPUtils.updatePassword(ldapStore, user, "Password1");
}
// Add dummy provider
dummyModel = appRealm.addUserFederationProvider(DummyUserFederationProviderFactory.PROVIDER_NAME, new HashMap<String, String>(), 1, "test-dummy", -1, 1, 0);
@ -122,9 +115,9 @@ public class SyncProvidersTest {
sleep(1000);
// Add user to LDAP and update 'user5' in LDAP
PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(session, ldapModel);
LDAPUtils.addUser(partitionManager, "user6", "User6FN", "User6LN", "user6@email.org");
LDAPUtils.updateUser(partitionManager, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
LDAPIdentityStore ldapStore = FederationProvidersIntegrationTest.getLdapIdentityStore(session, ldapModel);
LDAPUtils.addUser(ldapStore, "user6", "User6FN", "User6LN", "user6@email.org");
LDAPUtils.updateUser(ldapStore, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
// Assert still old users in local provider
assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");