From eb5ae4aae9dafb25fadb48ba82a9fdeceef9b103 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 7 Apr 2015 23:10:23 +0200 Subject: [PATCH] KEYCLOAK-1007 Fork Picketlink LDAP code. Remove picketlink dependencies from LDAP Federation provider --- dependencies/server-all/pom.xml | 28 - distribution/modules/build.xml | 8 - .../keycloak-ldap-federation/main/module.xml | 5 - .../keycloak-picketlink-api/main/module.xml | 21 - .../keycloak-picketlink-ldap/main/module.xml | 21 - .../keycloak/keycloak-server/main/module.xml | 2 - .../keycloak-services/main/module.xml | 2 - .../WEB-INF/jboss-deployment-structure.xml | 3 - federation/ldap/pom.xml | 27 - .../ldap/LDAPFederationProvider.java | 238 +++--- .../ldap/LDAPFederationProviderFactory.java | 59 +- .../ldap/LDAPIdentityStoreRegistry.java | 165 ++++ .../keycloak/federation/ldap/LDAPUtils.java | 150 ++-- .../ldap/WritableLDAPUserModelDelegate.java | 102 +-- .../idm/model/AbstractAttributedType.java | 85 ++ .../ldap/idm/model/AbstractIdentityType.java | 70 ++ .../federation/ldap/idm/model/Attribute.java | 80 ++ .../ldap/idm/model/AttributeProperty.java | 31 + .../ldap/idm/model/AttributedType.java | 75 ++ .../ldap/idm/model/IdentityType.java | 100 +++ .../federation/ldap/idm/model/LDAPUser.java | 85 ++ .../ldap/idm/query/AttributeParameter.java | 21 + .../federation/ldap/idm/query/Condition.java | 18 + .../ldap/idm/query/IdentityQuery.java | 225 ++++++ .../ldap/idm/query/IdentityQueryBuilder.java | 124 +++ .../ldap/idm/query/QueryParameter.java | 12 + .../federation/ldap/idm/query/Sort.java | 23 + .../idm/query/internal/BetweenCondition.java | 33 + .../query/internal/DefaultIdentityQuery.java | 207 +++++ .../query/internal/DefaultQueryBuilder.java | 89 ++ .../idm/query/internal/EqualCondition.java | 36 + .../query/internal/GreaterThanCondition.java | 34 + .../ldap/idm/query/internal/InCondition.java | 28 + .../idm/query/internal/LessThanCondition.java | 34 + .../idm/query/internal/LikeCondition.java | 28 + .../ldap/idm/store/IdentityStore.java | 81 ++ .../idm/store/ldap/LDAPIdentityStore.java | 761 ++++++++++++++++++ .../ldap/LDAPIdentityStoreConfiguration.java | 188 +++++ .../store/ldap/LDAPMappingConfiguration.java | 231 ++++++ .../idm/store/ldap/LDAPOperationManager.java | 606 ++++++++++++++ .../ldap/idm/store/ldap/LDAPUtil.java | 158 ++++ .../org/keycloak/models/LDAPConstants.java | 35 + .../reflection/NamedPropertyCriteria.java | 40 + .../reflection/TypedPropertyCriteria.java | 71 ++ picketlink/keycloak-picketlink-api/pom.xml | 55 -- .../picketlink/PartitionManagerProvider.java | 14 - .../PartitionManagerProviderFactory.java | 9 - .../picketlink/PartitionManagerSpi.java | 25 - .../services/org.keycloak.provider.Spi | 1 - picketlink/keycloak-picketlink-ldap/pom.xml | 66 -- .../picketlink/idm/KeycloakEventBridge.java | 63 -- .../idm/LDAPKeycloakCredentialHandler.java | 51 -- .../ldap/LDAPPartitionManagerProvider.java | 26 - .../LDAPPartitionManagerProviderFactory.java | 43 - .../ldap/PartitionManagerRegistry.java | 163 ---- ...picketlink.PartitionManagerProviderFactory | 1 - picketlink/pom.xml | 24 - pom.xml | 1 - services/pom.xml | 6 - .../FederationProvidersIntegrationTest.java | 36 +- .../federation/SyncProvidersTest.java | 31 +- 61 files changed, 4042 insertions(+), 1013 deletions(-) delete mode 100755 distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-api/main/module.xml delete mode 100755 distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-ldap/main/module.xml create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractAttributedType.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractIdentityType.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/Attribute.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributeProperty.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributedType.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/IdentityType.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPUser.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/AttributeParameter.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Condition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQuery.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQueryBuilder.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/QueryParameter.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Sort.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/BetweenCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultIdentityQuery.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultQueryBuilder.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/EqualCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/GreaterThanCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/InCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LessThanCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LikeCondition.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStoreConfiguration.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPMappingConfiguration.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPUtil.java create mode 100644 model/api/src/main/java/org/keycloak/models/utils/reflection/NamedPropertyCriteria.java create mode 100644 model/api/src/main/java/org/keycloak/models/utils/reflection/TypedPropertyCriteria.java delete mode 100755 picketlink/keycloak-picketlink-api/pom.xml delete mode 100644 picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java delete mode 100644 picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java delete mode 100644 picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java delete mode 100644 picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi delete mode 100755 picketlink/keycloak-picketlink-ldap/pom.xml delete mode 100755 picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java delete mode 100755 picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java delete mode 100644 picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java delete mode 100755 picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java delete mode 100755 picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java delete mode 100644 picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory delete mode 100755 picketlink/pom.xml diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 9d8921f8ed..d124ea5447 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -144,34 +144,6 @@ keycloak-kerberos-federation ${project.version} - - org.picketlink - picketlink-common - - - org.picketlink - picketlink-idm-api - - - org.picketlink - picketlink-idm-impl - - - org.picketlink - picketlink-idm-simple-schema - - - - - org.keycloak - keycloak-picketlink-api - ${project.version} - - - org.keycloak - keycloak-picketlink-ldap - ${project.version} - diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 9f65cb95d4..df4bef5917 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -259,14 +259,6 @@ - - - - - - - - diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-ldap-federation/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-ldap-federation/main/module.xml index 29dfd9c7d7..5f88f37e82 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-ldap-federation/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-ldap-federation/main/module.xml @@ -10,14 +10,9 @@ - - - - - diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-api/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-api/main/module.xml deleted file mode 100755 index b51112b48a..0000000000 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-api/main/module.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-ldap/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-ldap/main/module.xml deleted file mode 100755 index 429188fbb2..0000000000 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-ldap/main/module.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml index f553d24263..ddf24752d0 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml @@ -48,8 +48,6 @@ - - diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index 86e86f492e..9864a070c1 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -49,10 +49,8 @@ - - diff --git a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 6caa2c81e5..aae18bf302 100755 --- a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -40,9 +40,6 @@ - - - diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index a48b36daef..72803ab6e6 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -31,12 +31,6 @@ ${project.version} provided - - org.keycloak - keycloak-picketlink-api - ${project.version} - provided - org.jboss.resteasy resteasy-jaxrs @@ -61,27 +55,6 @@ jboss-logging provided - - - org.picketlink - picketlink-common - provided - - - org.picketlink - picketlink-idm-api - provided - - - org.picketlink - picketlink-idm-impl - provided - - - org.picketlink - picketlink-idm-simple-schema - provided - diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index f48880cfc9..370e0f07f8 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -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 supportedCredentialTypes = new HashSet(); - 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()); - return proxy(user); - } catch (IdentityManagementException ie) { - throw convertIDMException(ie); - } - + 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); } @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 searchByAttributes(Map attributes, RealmModel realm, int maxResults) { List searchResults =new LinkedList(); - try { - Map plUsers = searchPicketlink(attributes, maxResults); - for (User user : plUsers.values()) { - if (session.userStorage().getUserByUsername(user.getLoginName(), realm) == null) { - UserModel imported = importUserFromPicketlink(realm, user); - searchResults.add(imported); - } + + Map 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 searchPicketlink(Map attributes, int maxResults) { - IdentityManager identityManager = getIdentityManager(); - Map results = new HashMap(); + protected Map searchLDAP(Map attributes, int maxResults) { + + Map results = new HashMap(); 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 query = identityManager.createIdentityQuery(User.class); + IdentityQueryBuilder queryBuilder = this.ldapIdentityStore.createQueryBuilder(); + IdentityQuery 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 agents = query.getResultList(); - for (User user : agents) { + List 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) { - return false; - } - return picketlinkUser.getId().equals(local.getAttribute(LDAP_ID)); - } catch (IdentityManagementException ie) { - throw convertIDMException(ie); + LDAPUser ldapUser = LDAPUtils.getUser(this.ldapIdentityStore, local.getUsername()); + if (ldapUser == null) { + return false; } + 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) { - 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()); - return null; - } - - return importUserFromPicketlink(realm, picketlinkUser); - } catch (IdentityManagementException ie) { - throw convertIDMException(ie); - } - } - - public IdentityManager getIdentityManager() { - return partitionManager.createIdentityManager(); - } - - 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()); + LDAPUser ldapUser = LDAPUtils.getUser(this.ldapIdentityStore, username); + if (ldapUser == null) { + return null; } - UserModel imported = session.userStorage().addUser(realm, picketlinkUser.getLoginName()); + // KEYCLOAK-808: Should we allow case-sensitivity to be configurable? + 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 importUserFromLDAP(realm, ldapUser); + } + + 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()); + } + + 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) { - 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()); - return null; - } - - return importUserFromPicketlink(realm, picketlinkUser); - } catch (IdentityManagementException ie) { - throw convertIDMException(ie); + LDAPUser ldapUser = queryByEmail(email); + if (ldapUser == null) { + return null; } + + // KEYCLOAK-808: Should we allow case-sensitivity to be configurable? + 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 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 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 users, UserFederationProviderModel fedModel) { - for (User picketlinkUser : users) { - String username = picketlinkUser.getLoginName(); + protected void importLDAPUsers(RealmModel realm, List 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); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index c197052df9..a498a93b45 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -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 userQuery = partitionMgr.createIdentityManager().createIdentityQuery(User.class); + LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model); + IdentityQuery 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 userQuery = queryBuilder.createIdentityQuery(User.class).where(condition); + IdentityQuery 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 userQuery, final String realmId, final UserFederationProviderModel fedModel) { + protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery 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 users = userQuery.getResultList(); + final List 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 users = userQuery.getResultList(); + final List 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 users) { + protected void importLdapUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List 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) { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java new file mode 100644 index 0000000000..22aa55a6e9 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java @@ -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 Marek Posolda + */ +public class LDAPIdentityStoreRegistry { + + private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class); + + private Map ldapStores = new ConcurrentHashMap(); + + 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 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 ldapConfig) { + Map copy = new HashMap(ldapConfig); + copy.remove(LDAPConstants.BIND_CREDENTIAL); + logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderId + ", LDAP Configuration: " + copy); + } + + /** + * @param ldapConfig from realm + * @return PartitionManager instance based on LDAP store + */ + public static LDAPIdentityStore createLdapIdentityStore(Map 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(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 ldapConfig) { + String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES); + String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson, organizationalPerson"; + + String[] objectClasses = objClassesStr.split(","); + + // Trim them + String[] userObjectClasses = new String[objectClasses.length]; + for (int i=0 ; i config, LDAPIdentityStore store) { + this.config = config; + this.store = store; + } + + private Map config; + private LDAPIdentityStore store; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java index db0e9b8ab1..97535926c9 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java @@ -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 Marek Posolda */ @@ -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("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); + public static void updatePassword(LDAPIdentityStore ldapIdentityStore, LDAPUser user, String password) { + ldapIdentityStore.updatePassword(user, password); + } - 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 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); } - } - - public static User getUser(PartitionManager partitionManager, String username) { - IdentityManager idmManager = getIdentityManager(partitionManager); - return BasicModel.getUser(idmManager, username); + return ldapUser; } - public static User getUserByEmail(IdentityManager idmManager, String email) throws IdentityManagementException { - List agents = idmManager.createIdentityQuery(User.class) - .setParameter(User.EMAIL, email).getResultList(); + public static LDAPUser getUserByEmail(LDAPIdentityStore ldapIdentityStore, String email) { + IdentityQueryBuilder queryBuilder = ldapIdentityStore.createQueryBuilder(); + IdentityQuery query = queryBuilder.createIdentityQuery(LDAPUser.class) + .where(queryBuilder.equal(LDAPUser.EMAIL, email)); + List 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 users = idmManager.createIdentityQuery(User.class).getResultList(); + public static void removeAllUsers(LDAPIdentityStore ldapIdentityStore) { + List allUsers = getAllUsers(ldapIdentityStore); - for (User user : users) { - idmManager.remove(user); + for (LDAPUser user : allUsers) { + ldapIdentityStore.remove(user); } } - public static List 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 getAllUsers(LDAPIdentityStore ldapIdentityStore) { + IdentityQuery userQuery = ldapIdentityStore.createQueryBuilder().createIdentityQuery(LDAPUser.class); + return userQuery.getResultList(); } // Needed for ActiveDirectory updates diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java index 9a68f6a1ca..4debf0eaed 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java @@ -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 Bill Burke @@ -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) { - throw new IllegalStateException("User not found in LDAP storage!"); - } - picketlinkUser.setLoginName(username); - identityManager.update(picketlinkUser); - } catch (IdentityManagementException ie) { - throw new ModelException(ie); + LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername()); + if (ldapUser == null) { + throw new IllegalStateException("User not found in LDAP storage!"); } + 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) { - throw new IllegalStateException("User not found in LDAP storage!"); - } - picketlinkUser.setLastName(lastName); - identityManager.update(picketlinkUser); - } catch (IdentityManagementException ie) { - throw new ModelException(ie); + LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername()); + if (ldapUser == null) { + throw new IllegalStateException("User not found in LDAP storage!"); } + 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) { - throw new IllegalStateException("User not found in LDAP storage!"); - } - picketlinkUser.setFirstName(first); - identityManager.update(picketlinkUser); - } catch (IdentityManagementException ie) { - throw new ModelException(ie); + LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername()); + if (ldapUser == null) { + throw new IllegalStateException("User not found in LDAP storage!"); } + 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"); - } - 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); - } - } catch (IdentityManagementException ie) { - throw new ModelException(ie); + 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)) { + 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()); + } } @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) { - throw new IllegalStateException("User not found in LDAP storage!"); - } - picketlinkUser.setEmail(email); - identityManager.update(picketlinkUser); - } catch (IdentityManagementException ie) { - throw new ModelException(ie); + LDAPUser ldapUser = LDAPUtils.getUser(ldapIdentityStore, delegate.getUsername()); + if (ldapUser == null) { + throw new IllegalStateException("User not found in LDAP storage!"); } + ldapUser.setEmail(email); + ldapIdentityStore.update(ldapUser); + delegate.setEmail(email); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractAttributedType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractAttributedType.java new file mode 100644 index 0000000000..7e6d80b0f1 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractAttributedType.java @@ -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> attributes = + new HashMap>(); + + 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 attribute) { + attributes.put(attribute.getName(), attribute); + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + @SuppressWarnings("unchecked") + public Attribute getAttribute(String name) { + return (Attribute) attributes.get(name); + } + + public Collection> getAttributes() { + return unmodifiableCollection(attributes.values()); + } + + public Map> 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; + } + +} \ No newline at end of file diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractIdentityType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractIdentityType.java new file mode 100644 index 0000000000..8ee8bd6bd1 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractIdentityType.java @@ -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(); + } +} + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/Attribute.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/Attribute.java new file mode 100644 index 0000000000..82dac06b87 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/Attribute.java @@ -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 + */ +public class Attribute 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; + } +} + + + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributeProperty.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributeProperty.java new file mode 100644 index 0000000000..33b8706fca --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributeProperty.java @@ -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 { + + /** + *

Managed properties are stored as ad-hoc attributes and mapped from and to a specific property of a type.

+ * + * @return + */ + boolean managed() default false; + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributedType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributedType.java new file mode 100644 index 0000000000..5c374278c2 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributedType.java @@ -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 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 + */ + Attribute getAttribute(String name); + + /** + * Returns a Map containing all attribute values for this IdentityType instance. + * + * @return map of attribute names and their values + */ + Collection> getAttributes(); + + public final class QUERY_ATTRIBUTE { + public static AttributeParameter byName(String name) { + return new AttributeParameter(name); + } + } +} + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/IdentityType.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/IdentityType.java new file mode 100644 index 0000000000..f8ae8a1833 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/IdentityType.java @@ -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(); + + /** + *

Sets the current enabled status of this {@link IdentityType}.

+ * + * @param enabled + */ + void setEnabled(boolean enabled); + + /** + * Returns the date that this IdentityType instance was created. + * + * @return Date value representing the creation date + */ + Date getCreatedDate(); + + /** + *

Sets the date that this {@link IdentityType} was created.

+ * + * @param createdDate + */ + void setCreatedDate(Date createdDate); + + /** + * Returns the date that this IdentityType expires, or null if there is no expiry date. + * + * @return + */ + Date getExpirationDate(); + + /** + *

Sets the date that this {@link IdentityType} expires.

+ * + * @param expirationDate + */ + void setExpirationDate(Date expirationDate); + +} + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPUser.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPUser.java new file mode 100644 index 0000000000..4ce7ef9516 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPUser.java @@ -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; + } + +} + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/AttributeParameter.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/AttributeParameter.java new file mode 100644 index 0000000000..c5feea9319 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/AttributeParameter.java @@ -0,0 +1,21 @@ +package org.keycloak.federation.ldap.idm.query; + +/** + *

This class can be used to define a query parameter for properties annotated with + * {@link org.keycloak.federation.ldap.idm.model.AttributeProperty}. + *

+ * + * @author pedroigor + */ +public class AttributeParameter implements QueryParameter { + + private final String name; + + public AttributeParameter(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Condition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Condition.java new file mode 100644 index 0000000000..85d81d8915 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Condition.java @@ -0,0 +1,18 @@ +package org.keycloak.federation.ldap.idm.query; + +/** + *

A {@link Condition} is used to specify how a specific {@link QueryParameter} + * is defined in order to filter query results.

+ * + * @author Pedro Igor + */ +public interface Condition { + + /** + *

The {@link QueryParameter} restricted by this condition.

+ * + * @return + */ + QueryParameter getParameter(); + +} \ No newline at end of file diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQuery.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQuery.java new file mode 100644 index 0000000000..1a77727fa8 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQuery.java @@ -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; + +/** + *

An {@link IdentityQuery} is responsible for querying the underlying identity stores for instances of + * a given {@link IdentityType}.

+ * + *

Instances of this class are obtained using the {@link IdentityQueryBuilder#createIdentityQuery(Class)} + * method.

+ * + *
+ *      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 result = query.getResultList();
+ * 
+ * + *

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.

+ * + * @author Shane Bryzak + * @author Pedro Igor + */ +public interface IdentityQuery { + + /** + * @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 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 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 setSortAscending(boolean sortAscending); + + /** + *

Set a query parameter to this query in order to filter the results.

+ * + *

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.

+ * + * @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 setParameter(QueryParameter param, Object... value); + + /** + *

Add to this query the conditions that will be used to filter results.

+ * + *

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.

+ * + * @param condition One or more conditions created from {@link IdentityQueryBuilder}. + * + * @return + */ + IdentityQuery where(Condition... condition); + + /** + *

Add to this query the sorting conditions to be applied to the results.

+ * + * @param sorts The ordering conditions. + * + * @return + */ + IdentityQuery sortBy(Sort... sorts); + + /** + *

The type used to create this query.

+ * + * @return + */ + Class getIdentityType(); + + /** + *

Returns a map with all the parameter set for this query.

+ * + * @return + * + * @deprecated Use {@link IdentityQuery#getConditions()} instead. Will be removed. + */ + @Deprecated + Map getParameters(); + + /** + *

Returns a set containing all conditions used by this query to filter its results.

+ * + * @return + */ + Set getConditions(); + + /** + *

Returns a set containing all sorting conditions used to filter the results.

+ * + * @return + */ + Set getSorting(); + + /** + *

Returns the value used to restrict the given query parameter.

+ * + * @param queryParameter + * + * @return + */ + @Deprecated + Object[] getParameter(QueryParameter queryParameter); + + @Deprecated + Map getParameters(Class type); + + int getOffset(); + + /** + *

Set the position of the first result to retrieve.

+ * + * @param offset + * + * @return + */ + IdentityQuery setOffset(int offset); + + /** + *

Returns the number of instances to retrieve.

+ * + * @return + */ + int getLimit(); + + /** + *

Set the maximum number of results to retrieve.

+ * + * @param limit the number of instances to retrieve. + * + * @return + */ + IdentityQuery setLimit(int limit); + + /** + *

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.

+ * + * @return + */ + List 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(); +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQueryBuilder.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQueryBuilder.java new file mode 100644 index 0000000000..022063521b --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQueryBuilder.java @@ -0,0 +1,124 @@ +package org.keycloak.federation.ldap.idm.query; + +import org.keycloak.federation.ldap.idm.model.IdentityType; + +/** + *

The {@link IdentityQueryBuilder} is responsible for creating {@link IdentityQuery} instances and also + * provide methods to create conditions, orderings, sorting, etc.

+ * + * @author Pedro Igor + */ +public interface IdentityQueryBuilder { + + /** + *

Create a condition for testing the whether the query parameter satisfies the given pattern..

+ * + * @param parameter The query parameter. + * @param pattern The pattern to match. + * + * @return + */ + Condition like(QueryParameter parameter, String pattern); + + /** + *

Create a condition for testing the arguments for equality.

+ * + * @param parameter The query parameter. + * @param value The value to compare. + * + * @return + */ + Condition equal(QueryParameter parameter, Object value); + + /** + *

Create a condition for testing whether the query parameter is grater than the given value..

+ * + * @param parameter The query parameter. + * @param x The value to compare. + * + * @return + */ + Condition greaterThan(QueryParameter parameter, Object x); + + /** + *

Create a condition for testing whether the query parameter is grater than or equal to the given value..

+ * + * @param parameter The query parameter. + * @param x The value to compare. + * + * @return + */ + Condition greaterThanOrEqualTo(QueryParameter parameter, Object x); + + /** + *

Create a condition for testing whether the query parameter is less than the given value..

+ * + * @param parameter The query parameter. + * @param x The value to compare. + * + * @return + */ + Condition lessThan(QueryParameter parameter, Object x); + + /** + *

Create a condition for testing whether the query parameter is less than or equal to the given value..

+ * + * @param parameter The query parameter. + * @param x The value to compare. + * + * @return + */ + Condition lessThanOrEqualTo(QueryParameter parameter, Object x); + + /** + *

Create a condition for testing whether the query parameter is between the given values.

+ * + * @param parameter The query parameter. + * @param x The first value. + * @param x The second value. + * + * @return + */ + Condition between(QueryParameter parameter, Object x, Object y); + + /** + *

Create a condition for testing whether the query parameter is contained in a list of values.

+ * + * @param parameter The query parameter. + * @param values A list of values. + * + * @return + */ + Condition in(QueryParameter parameter, Object... values); + + /** + *

Create an ascending order for the given parameter. Once created, you can use it to sort the results of a + * query.

+ * + * @param parameter The query parameter to sort. + * + * @return + */ + Sort asc(QueryParameter parameter); + + /** + *

Create an descending order for the given parameter. Once created, you can use it to sort the results of a + * query.

+ * + * @param parameter The query parameter to sort. + * + * @return + */ + Sort desc(QueryParameter parameter); + + /** + *

Create an {@link IdentityQuery} that can be used to query for {@link + * IdentityType} instances of a the given identityType.

+ * + * @param identityType The type to search. If you provide the {@link IdentityType} + * base interface any of its sub-types will be returned. + * + * @return + */ + IdentityQuery createIdentityQuery(Class identityType); +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/QueryParameter.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/QueryParameter.java new file mode 100644 index 0000000000..ae2bbdf910 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/QueryParameter.java @@ -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 { + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Sort.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Sort.java new file mode 100644 index 0000000000..dfd331ed59 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/Sort.java @@ -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; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/BetweenCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/BetweenCondition.java new file mode 100644 index 0000000000..672fdaa72b --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/BetweenCondition.java @@ -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; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultIdentityQuery.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultIdentityQuery.java new file mode 100644 index 0000000000..21b02537da --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultIdentityQuery.java @@ -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 + * + * @author Shane Bryzak + */ +public class DefaultIdentityQuery implements IdentityQuery { + + private final Map parameters = new LinkedHashMap(); + private final Class 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 conditions = new LinkedHashSet(); + private final Set ordering = new LinkedHashSet(); + + public DefaultIdentityQuery(IdentityQueryBuilder queryBuilder, Class identityType, IdentityStore identityStore) { + this.queryBuilder = queryBuilder; + this.identityStore = identityStore; + this.identityType = identityType; + } + + @Override + public IdentityQuery 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 where(Condition... condition) { + this.conditions.addAll(Arrays.asList(condition)); + return this; + } + + @Override + public IdentityQuery sortBy(Sort... sorts) { + this.ordering.addAll(Arrays.asList(sorts)); + return this; + } + + @Override + public Set getSorting() { + return unmodifiableSet(this.ordering); + } + + @Override + public Class getIdentityType() { + return identityType; + } + + @Override + public Map getParameters() { + return parameters; + } + + @Override + public Object[] getParameter(QueryParameter queryParameter) { + return this.parameters.get(queryParameter); + } + + @Override + public Map getParameters(Class type) { + Map typedParameters = new HashMap(); + + Set> entrySet = this.parameters.entrySet(); + + for (Map.Entry 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 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 result = new ArrayList(); + + 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 setOffset(int offset) { + this.offset = offset; + return this; + } + + @Override + public IdentityQuery setLimit(int limit) { + this.limit = limit; + return this; + } + + @Override + public IdentityQuery setSortParameters(QueryParameter... sortParameters) { + this.sortParameters = sortParameters; + return this; + } + + @Override + public IdentityQuery setSortAscending(boolean sortAscending) { + this.sortAscending = sortAscending; + return this; + } + + @Override + public IdentityQuery setPaginationContext(Object object) { + this.paginationContext = object; + return this; + } + + @Override + public Set getConditions() { + return unmodifiableSet(this.conditions); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultQueryBuilder.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultQueryBuilder.java new file mode 100644 index 0000000000..5d3b72e165 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultQueryBuilder.java @@ -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 IdentityQuery createIdentityQuery(Class 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 + "."); + } + } +} \ No newline at end of file diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/EqualCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/EqualCondition.java new file mode 100644 index 0000000000..a3fee26e13 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/EqualCondition.java @@ -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 + + '}'; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/GreaterThanCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/GreaterThanCondition.java new file mode 100644 index 0000000000..cbdf5407b6 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/GreaterThanCondition.java @@ -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; + } +} \ No newline at end of file diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/InCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/InCondition.java new file mode 100644 index 0000000000..54d22342a9 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/InCondition.java @@ -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; + } +} + diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LessThanCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LessThanCondition.java new file mode 100644 index 0000000000..5906a5ced0 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LessThanCondition.java @@ -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; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LikeCondition.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LikeCondition.java new file mode 100644 index 0000000000..6c6810362b --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LikeCondition.java @@ -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; + } + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java new file mode 100644 index 0000000000..7fef705c86 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java @@ -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 + + List fetchQueryResults(IdentityQuery identityQuery); + + int countQueryResults(IdentityQuery identityQuery); + +// // Relationship query +// +// List fetchQueryResults(RelationshipQuery query); +// +// int countQueryResults(RelationshipQuery 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); + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java new file mode 100644 index 0000000000..8b912406ac --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -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 Pedro Silva + */ +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 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 List fetchQueryResults(IdentityQuery identityQuery) { + List results = new ArrayList(); + + 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 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 int countQueryResults(IdentityQuery 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 modItems = new ArrayList(); + 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 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 StringBuilder createIdentityTypeSearchFilter(final IdentityQuery 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 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 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 search = this.operationManager.search(getConfig().getBaseDN(), filter.toString(), entryConfig); + + if (!search.isEmpty()) { + SearchResult next = search.get(0); + + Property parentProperty = PropertyQueries + .createQuery(attributedType.getClass()) + .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult(); + + if (parentProperty != null) { + String parentDN = next.getNameInNamespace(); + String parentBaseDN = parentDN.substring(parentDN.indexOf(",") + 1); + Class 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 attrMapping, String ldapAttributeName) { + for (Map.Entry currentAttr : attrMapping.entrySet()) { + if (currentAttr.getValue().equalsIgnoreCase(ldapAttributeName)) { + return currentAttr.getKey(); + } + } + + return null; + } + + private List getEntryObjectClasses(final Attributes attributes) throws NamingException { + Attribute objectClassesAttribute = attributes.get(LDAPConstants.OBJECT_CLASS); + List objectClasses = new ArrayList(); + + 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 mappedProperties = mappingConfig.getMappedProperties(); + + for (String propertyName : mappedProperties.keySet()) { + if (!mappingConfig.getReadOnlyAttributes().contains(propertyName) && (isCreate || !mappingConfig.getBindingProperty().getName().equals(propertyName))) { + Property property = PropertyQueries + .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 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 idProperty = mappingConfig.getIdProperty(); + + String baseDN; + + if (mappingConfig.getBaseDN() == null || !appendBaseDN) { + baseDN = ""; + } else { + baseDN = LDAPConstants.COMMA + getBaseDN(attributedType); + } + + Property 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 parentProperty = PropertyQueries + .createQuery(attributedType.getClass()) + .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult(); + + if (parentProperty != null) { + AttributedType parentType = parentProperty.getValue(attributedType); + + if (parentType != null) { + Property 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 parentProperty = PropertyQueries + .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 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); + } + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStoreConfiguration.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStoreConfiguration.java new file mode 100644 index 0000000000..0c0a2e178e --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStoreConfiguration.java @@ -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, LDAPMappingConfiguration> mappingConfig = new HashMap, 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 clazz) { + LDAPMappingConfiguration mappingConfig = new LDAPMappingConfiguration(clazz); + this.mappingConfig.put(clazz, mappingConfig); + return mappingConfig; + } + + public Class getSupportedTypeByBaseDN(String entryDN, List 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 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; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPMappingConfiguration.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPMappingConfiguration.java new file mode 100644 index 0000000000..033d93bbf2 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPMappingConfiguration.java @@ -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 mappedClass; + private Set objectClasses; + private String baseDN; + private final Map mappedProperties = new HashMap(); + private Property idProperty; + private Class relatedAttributedType; + private String parentMembershipAttributeName; + private Map parentMapping = new HashMap(); + private final Set readOnlyAttributes = new HashSet(); + private int hierarchySearchDepth; + private Property bindingProperty; + + public LDAPMappingConfiguration(Class mappedClass) { + this.mappedClass = mappedClass; + } + + public Class getMappedClass() { + return this.mappedClass; + } + + public Set getObjectClasses() { + return this.objectClasses; + } + + public String getBaseDN() { + return this.baseDN; + } + + public Map getMappedProperties() { + return this.mappedProperties; + } + + public Property getIdProperty() { + return this.idProperty; + } + + public Property getBindingProperty() { + return this.bindingProperty; + } + + public Class getRelatedAttributedType() { + return this.relatedAttributedType; + } + + public String getParentMembershipAttributeName() { + return this.parentMembershipAttributeName; + } + + public Map getParentMapping() { + return this.parentMapping; + } + + public Set getReadOnlyAttributes() { + return this.readOnlyAttributes; + } + + public int getHierarchySearchDepth() { + return this.hierarchySearchDepth; + } + + private Property getBindingProperty(final String bindingPropertyName) { + Property bindingProperty = PropertyQueries + .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() { + + @Override + public String getName() { + return bindingPropertyName; + } + + @Override + public Type getBaseType() { + return null; + } + + @Override + public Class 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 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 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 + .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 relatedAttributedType) { + this.relatedAttributedType = relatedAttributedType; + return this; + } + + public LDAPMappingConfiguration setParentMembershipAttributeName(String parentMembershipAttributeName) { + this.parentMembershipAttributeName = parentMembershipAttributeName; + return this; + } + + public LDAPMappingConfiguration setParentMapping(Map 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; + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java new file mode 100644 index 0000000000..507d61fc3f --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java @@ -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; + +/** + *

This class provides a set of operations to manage LDAP trees.

+ * + * @author Anil Saldhana + * @author Pedro Silva + */ +public class LDAPOperationManager { + + private static final Logger logger = Logger.getLogger(LDAPOperationManager.class); + + private final LDAPIdentityStoreConfiguration config; + private final Map connectionProperties; + + public LDAPOperationManager(LDAPIdentityStoreConfiguration config) throws NamingException { + this.config = config; + this.connectionProperties = Collections.unmodifiableMap(createConnectionProperties()); + } + + /** + *

+ * Modifies the given {@link javax.naming.directory.Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE + * operation. + *

+ * + * @param dn + * @param attribute + */ + public void modifyAttribute(String dn, Attribute attribute) { + ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)}; + modifyAttributes(dn, mods); + } + + /** + *

+ * Modifies the given {@link Attribute} instances using the given DN. This method performs a REPLACE_ATTRIBUTE + * operation. + *

+ * + * @param dn + * @param attributes + */ + public void modifyAttributes(String dn, NamingEnumeration attributes) { + try { + List modItems = new ArrayList(); + 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); + } + + } + + /** + *

+ * Removes the given {@link Attribute} instance using the given DN. This method performs a REMOVE_ATTRIBUTE + * operation. + *

+ * + * @param dn + * @param attribute + */ + public void removeAttribute(String dn, Attribute attribute) { + ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)}; + modifyAttributes(dn, mods); + } + + /** + *

+ * Adds the given {@link Attribute} instance using the given DN. This method performs a ADD_ATTRIBUTE operation. + *

+ * + * @param dn + * @param attribute + */ + public void addAttribute(String dn, Attribute attribute) { + ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)}; + modifyAttributes(dn, mods); + } + + /** + *

+ * Searches the LDAP tree. + *

+ * + * @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() { + @Override + public SearchResult execute(LdapContext context) throws NamingException { + NamingEnumeration 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 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 search(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration) throws NamingException { + final List result = new ArrayList(); + final SearchControls cons = getSearchControls(mappingConfiguration); + + try { + return execute(new LdapOperation>() { + @Override + public List execute(LdapContext context) throws NamingException { + NamingEnumeration 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 List searchPaginated(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration, final IdentityQuery identityQuery) throws NamingException { + final List result = new ArrayList(); + final SearchControls cons = getSearchControls(mappingConfiguration); + + try { + return execute(new LdapOperation>() { + @Override + public List 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 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 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 = ""; + + try { + Attributes attributes = execute(new LdapOperation() { + @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() { + @Override + public SearchResult execute(LdapContext context) throws NamingException { + NamingEnumeration 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); + } + } + + /** + *

+ * Destroys a subcontext with the given DN from the LDAP tree. + *

+ * + * @param dn + */ + private void destroySubcontext(LdapContext context, final String dn) { + try { + NamingEnumeration 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); + } + } + + /** + *

+ * Performs a simple authentication using the given DN and password to bind to the authentication context. + *

+ * + * @param dn + * @param password + * + * @return + */ + public boolean authenticate(String dn, String password) { + InitialContext authCtx = null; + + try { + Hashtable env = new Hashtable(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() { + @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 all = attributes.getAll(); + + while (all.hasMore()) { + Attribute attribute = all.next(); + + logger.debugf(" %s = %s", attribute.getID(), attribute.get()); + } + + logger.debugf("]"); + } + + execute(new LdapOperation() { + @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 createEmptyEnumeration() { + return new NamingEnumeration() { + @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(this.connectionProperties), null); + } + + private Map createConnectionProperties() { + HashMap env = new HashMap(); + + 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 execute(LdapOperation 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 execute(LdapContext context) throws NamingException; + } + + private List getReturningAttributes(final LDAPMappingConfiguration mappingConfiguration) { + List returningAttributes = new ArrayList(); + + 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; + } +} \ No newline at end of file diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPUtil.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPUtil.java new file mode 100644 index 0000000000..f08ff855b2 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPUtil.java @@ -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; + +/** + *

Utility class for working with LDAP.

+ * + * @author Pedro Igor + */ +public class LDAPUtil { + + /** + *

Formats the given date.

+ * + * @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); + } + + /** + *

+ * Parses dates/time stamps stored in LDAP. Some possible values: + *

+ *
    + *
  • 20020228150820
  • + *
  • 20030228150820Z
  • + *
  • 20050228150820.12
  • + *
  • 20060711011740.0Z
  • + *
+ * + * @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); + } + } + + + + /** + *

Creates a byte-based {@link String} representation of a raw byte array representing the value of the + * objectGUID attribute retrieved from Active Directory.

+ * + *

The returned string is useful to perform queries on AD based on the objectGUID value. Eg.:

+ * + *

+ * String filter = "(&(objectClass=*)(objectGUID" + EQUAL + convertObjectGUIToByteString(objectGUID) + "))"; + *

+ * + * @param objectGUID A raw byte array representing the value of the objectGUID 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(); + } + + /** + *

Decode a raw byte array representing the value of the objectGUID attribute retrieved from Active + * Directory.

+ * + *

The returned string is useful to directly bind an entry. Eg.:

+ * + *

+ * String bindingString = decodeObjectGUID(objectGUID); + *
+ * Attributes attributes = ctx.getAttributes(bindingString); + *

+ * + * @param objectGUID A raw byte array representing the value of the objectGUID 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); + } + } + + +} diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java index bf76be7be9..acab3e0d6e 100644 --- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java +++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java @@ -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"; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/reflection/NamedPropertyCriteria.java b/model/api/src/main/java/org/keycloak/models/utils/reflection/NamedPropertyCriteria.java new file mode 100644 index 0000000000..fc3b538d45 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/reflection/NamedPropertyCriteria.java @@ -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; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/reflection/TypedPropertyCriteria.java b/model/api/src/main/java/org/keycloak/models/utils/reflection/TypedPropertyCriteria.java new file mode 100644 index 0000000000..93688a4102 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/reflection/TypedPropertyCriteria.java @@ -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 { + + /** + *

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 propertyClass it will be selected.

  • SUB_TYPE: + * Also consider properties where its type is a subtype of propertyClass. .
  • SUPER_TYPE: Also + * consider properties where its type is a superclass or superinterface of propertyClass. .
+ *

+ */ + 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; + } +} + diff --git a/picketlink/keycloak-picketlink-api/pom.xml b/picketlink/keycloak-picketlink-api/pom.xml deleted file mode 100755 index 5655ce9516..0000000000 --- a/picketlink/keycloak-picketlink-api/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - keycloak-picketlink-parent - org.keycloak - 1.2.0.RC1-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-picketlink-api - Keycloak Picketlink API - - - - - org.keycloak - keycloak-core - ${project.version} - provided - - - org.keycloak - keycloak-model-api - ${project.version} - provided - - - org.picketlink - picketlink-idm-api - provided - - - org.jboss.logging - jboss-logging - provided - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${maven.compiler.source} - ${maven.compiler.target} - - - - - - diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java deleted file mode 100644 index 0b22305364..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.keycloak.picketlink; - -import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.provider.Provider; -import org.picketlink.idm.PartitionManager; - -/** - * - * @author Marek Posolda - */ -public interface PartitionManagerProvider extends Provider { - - PartitionManager getPartitionManager(UserFederationProviderModel model); -} diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java deleted file mode 100644 index 203c7f9c23..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.keycloak.picketlink; - -import org.keycloak.provider.ProviderFactory; - -/** - * @author Marek Posolda - */ -public interface PartitionManagerProviderFactory extends ProviderFactory { -} diff --git a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java b/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java deleted file mode 100644 index 721cf12d68..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.keycloak.picketlink; - -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; -import org.keycloak.provider.Spi; - -/** - * @author Stian Thorgersen - */ -public class PartitionManagerSpi implements Spi { - @Override - public String getName() { - return "picketlink-idm"; - } - - @Override - public Class getProviderClass() { - return PartitionManagerProvider.class; - } - - @Override - public Class getProviderFactoryClass() { - return PartitionManagerProviderFactory.class; - } -} diff --git a/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi deleted file mode 100644 index 88b1bcc076..0000000000 --- a/picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.picketlink.PartitionManagerSpi \ No newline at end of file diff --git a/picketlink/keycloak-picketlink-ldap/pom.xml b/picketlink/keycloak-picketlink-ldap/pom.xml deleted file mode 100755 index 921637699d..0000000000 --- a/picketlink/keycloak-picketlink-ldap/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - keycloak-picketlink-parent - org.keycloak - 1.2.0.RC1-SNAPSHOT - ../pom.xml - - 4.0.0 - - keycloak-picketlink-ldap - Keycloak Picketlink LDAP - - - - - org.keycloak - keycloak-core - ${project.version} - provided - - - org.keycloak - keycloak-model-api - ${project.version} - provided - - - org.keycloak - keycloak-picketlink-api - ${project.version} - provided - - - org.picketlink - picketlink-idm-api - provided - - - org.picketlink - picketlink-idm-impl - provided - - - org.jboss.logging - jboss-logging - provided - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${maven.compiler.source} - ${maven.compiler.target} - - - - - - diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java deleted file mode 100755 index 1fd7f159d8..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java +++ /dev/null @@ -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 Marek Posolda - */ -public class KeycloakEventBridge implements EventBridge { - - private static final Logger logger = Logger.getLogger(KeycloakEventBridge.class); - - private final boolean updateUserAccountAfterPasswordUpdate; - - public KeycloakEventBridge(boolean updateUserAccountAfterPasswordUpdate) { - this.updateUserAccountAfterPasswordUpdate = updateUserAccountAfterPasswordUpdate; - 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"); - } - - } - } -} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java deleted file mode 100755 index bc5278c316..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java +++ /dev/null @@ -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 Marek Posolda - */ -public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredentialHandler { - - // Overridden as in Keycloak, we don't have Agents - @Override - protected User getAccount(IdentityContext context, String loginName) { - IdentityManager identityManager = getIdentityManager(context); - - if (CREDENTIAL_LOGGER.isDebugEnabled()) { - CREDENTIAL_LOGGER.debugf("Trying to find account [%s] using default account type [%s]", loginName, User.class); - } - - return BasicModel.getUser(identityManager, loginName); - } - - - @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; - } -} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java deleted file mode 100644 index bbb7201e69..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java +++ /dev/null @@ -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 Marek Posolda - */ -public class LDAPPartitionManagerProvider implements PartitionManagerProvider { - - private final PartitionManagerRegistry partitionManagerRegistry; - - public LDAPPartitionManagerProvider(PartitionManagerRegistry partitionManagerRegistry) { - this.partitionManagerRegistry = partitionManagerRegistry; - } - - @Override - public PartitionManager getPartitionManager(UserFederationProviderModel model) { - return partitionManagerRegistry.getPartitionManager(model); - } - - @Override - public void close() { - } -} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java deleted file mode 100755 index 851201d8f0..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java +++ /dev/null @@ -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 Marek Posolda - */ -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"; - } - -} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java deleted file mode 100755 index a120ee1876..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java +++ /dev/null @@ -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 Marek Posolda - */ -public class PartitionManagerRegistry { - - private static final Logger logger = Logger.getLogger(PartitionManagerRegistry.class); - - private Map partitionManagers = new ConcurrentHashMap(); - - 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 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 ldapConfig) { - Map copy = new HashMap(ldapConfig); - copy.remove(LDAPConstants.BIND_CREDENTIAL); - logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderId + ", LDAP Configuration: " + copy); - } - - /** - * @param ldapConfig from realm - * @return PartitionManager instance based on LDAP store - */ - public static PartitionManager createPartitionManager(Map 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 ldapConfig) { - String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES); - String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson, organizationalPerson"; - - String[] objectClasses = objClassesStr.split(","); - - // Trim them - String[] userObjectClasses = new String[objectClasses.length]; - for (int i=0 ; i config, PartitionManager manager) { - this.config = config; - this.partitionManager = manager; - } - - private Map config; - private PartitionManager partitionManager; - } -} diff --git a/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory b/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory deleted file mode 100644 index 5bfaf9d173..0000000000 --- a/picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.picketlink.ldap.LDAPPartitionManagerProviderFactory \ No newline at end of file diff --git a/picketlink/pom.xml b/picketlink/pom.xml deleted file mode 100755 index 10f193561d..0000000000 --- a/picketlink/pom.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - keycloak-parent - org.keycloak - 1.2.0.RC1-SNAPSHOT - ../pom.xml - - 4.0.0 - pom - - keycloak-picketlink-parent - Keycloak Picketlink - - - - keycloak-picketlink-api - keycloak-picketlink-ldap - - - - diff --git a/pom.xml b/pom.xml index f3f68b0414..87ac6079b2 100755 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,6 @@ model integration proxy - picketlink federation services saml diff --git a/services/pom.xml b/services/pom.xml index e5fde97b37..ea6e90e600 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -102,12 +102,6 @@ ${project.version} provided - - org.keycloak - keycloak-picketlink-api - ${project.version} - provided - org.jboss.spec.javax.servlet jboss-servlet-api_3.0_spec diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index cfaae07ec8..929029eef5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -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 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(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index 55f68bb656..f628519af7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -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 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(), 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");