keycloak-aplcache
Changes
dependencies/server-all/pom.xml 28(+0 -28)
distribution/modules/build.xml 8(+0 -8)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-ldap-federation/main/module.xml 5(+0 -5)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-api/main/module.xml 21(+0 -21)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-picketlink-ldap/main/module.xml 21(+0 -21)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml 2(+0 -2)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml 2(+0 -2)
federation/ldap/pom.xml 27(+0 -27)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractAttributedType.java 85(+85 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AbstractIdentityType.java 70(+70 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/AttributeProperty.java 31(+31 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/AttributeParameter.java 21(+21 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQuery.java 225(+225 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/IdentityQueryBuilder.java 124(+124 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/BetweenCondition.java 33(+33 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultIdentityQuery.java 207(+207 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/DefaultQueryBuilder.java 89(+89 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/EqualCondition.java 36(+36 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/GreaterThanCondition.java 34(+34 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/InCondition.java 28(+28 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LessThanCondition.java 34(+34 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LikeCondition.java 28(+28 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java 761(+761 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStoreConfiguration.java 188(+188 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPMappingConfiguration.java 231(+231 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java 606(+606 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPUtil.java 158(+158 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java 224(+90 -134)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java 59(+26 -33)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPIdentityStoreRegistry.java 165(+165 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java 112(+44 -68)
picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProvider.java 14(+0 -14)
picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerProviderFactory.java 9(+0 -9)
picketlink/keycloak-picketlink-api/src/main/java/org/keycloak/picketlink/PartitionManagerSpi.java 25(+0 -25)
picketlink/keycloak-picketlink-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi 1(+0 -1)
picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/KeycloakEventBridge.java 63(+0 -63)
picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java 51(+0 -51)
picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProvider.java 26(+0 -26)
picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/LDAPPartitionManagerProviderFactory.java 43(+0 -43)
picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java 163(+0 -163)
picketlink/keycloak-picketlink-ldap/src/main/resources/META-INF/services/org.keycloak.picketlink.PartitionManagerProviderFactory 1(+0 -1)
pom.xml 1(+0 -1)
services/pom.xml 6(+0 -6)
Details
dependencies/server-all/pom.xml 28(+0 -28)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 9d8921f..d124ea5 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -144,34 +144,6 @@
<artifactId>keycloak-kerberos-federation</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-common</artifactId>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-impl</artifactId>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-simple-schema</artifactId>
- </dependency>
-
- <!-- picketlink -->
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-picketlink-api</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-picketlink-ldap</artifactId>
- <version>${project.version}</version>
- </dependency>
<!-- saml -->
<dependency>
distribution/modules/build.xml 8(+0 -8)
diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml
index 9f65cb9..df4bef5 100755
--- a/distribution/modules/build.xml
+++ b/distribution/modules/build.xml
@@ -259,14 +259,6 @@
<maven-resource group="org.keycloak" artifact="keycloak-ldap-federation"/>
</module-def>
- <module-def name="org.keycloak.keycloak-picketlink-api">
- <maven-resource group="org.keycloak" artifact="keycloak-picketlink-api"/>
- </module-def>
-
- <module-def name="org.keycloak.keycloak-picketlink-ldap">
- <maven-resource group="org.keycloak" artifact="keycloak-picketlink-ldap"/>
- </module-def>
-
<module-def name="org.keycloak.keycloak-saml-core">
<maven-resource group="org.keycloak" artifact="keycloak-saml-core"/>
</module-def>
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 29dfd9c..5f88f37 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 @@
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-kerberos-federation"/>
- <module name="org.keycloak.keycloak-picketlink-api"/>
<module name="javax.ws.rs.api"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
<module name="org.jboss.logging"/>
- <module name="org.picketlink.common"/>
- <module name="org.picketlink.idm.api"/>
- <module name="org.picketlink.idm"/>
- <module name="org.picketlink.idm.schema"/>
<module name="javax.api"/>
</dependencies>
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 f553d24..ddf2475 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 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
- <module name="org.keycloak.keycloak-picketlink-api" services="import"/>
- <module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>
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 86e86f4..9864a07 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 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
- <module name="org.keycloak.keycloak-picketlink-api" services="import"/>
<module name="org.keycloak.keycloak-wildfly-extensions" services="import"/>
- <module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>
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 6caa2c8..aae18bf 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 @@
<module name="org.keycloak.keycloak-model-sessions-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
- <module name="org.keycloak.keycloak-picketlink-api" services="import"/>
-
- <module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-social-core" services="import"/>
federation/ldap/pom.xml 27(+0 -27)
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index a48b36d..72803ab 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -32,12 +32,6 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-picketlink-api</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
@@ -61,27 +55,6 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
-
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-common</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-api</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-impl</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-simple-schema</artifactId>
- <scope>provided</scope>
- </dependency>
</dependencies>
<build>
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 0000000..7e6d80b
--- /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<String, Attribute<? extends Serializable>> attributes =
+ new HashMap<String, Attribute<? extends Serializable>>();
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getEntryDN() {
+ return entryDN;
+ }
+
+ public void setEntryDN(String entryDN) {
+ this.entryDN = entryDN;
+ }
+
+ public void setAttribute(Attribute<? extends Serializable> attribute) {
+ attributes.put(attribute.getName(), attribute);
+ }
+
+ public void removeAttribute(String name) {
+ attributes.remove(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends Serializable> Attribute<T> getAttribute(String name) {
+ return (Attribute<T>) attributes.get(name);
+ }
+
+ public Collection<Attribute<? extends Serializable>> getAttributes() {
+ return unmodifiableCollection(attributes.values());
+ }
+
+ public Map<String,Attribute<? extends Serializable>> getAttributesMap() {
+ return unmodifiableMap(attributes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (!getClass().isInstance(obj)) {
+ return false;
+ }
+
+ AttributedType other = (AttributedType) obj;
+
+ return getId() != null && other.getId() != null && getId().equals(other.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getId() != null ? getId().hashCode() : 0;
+ result = 31 * result + (getId() != null ? getId().hashCode() : 0);
+ return result;
+ }
+
+}
\ 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 0000000..8ee8bd6
--- /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 0000000..82dac06
--- /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 <T>
+ */
+public class Attribute<T extends Serializable> implements Serializable {
+
+ private static final long serialVersionUID = 237211288303510728L;
+
+ /**
+ * The name of the attribute
+ */
+ private String name;
+
+ /**
+ * The attribute value.
+ */
+ private T value;
+
+ /**
+ * Indicates whether this Attribute has a read-only value
+ */
+ private boolean readOnly = false;
+
+ /**
+ * Indicates whether the Attribute value has been loaded
+ */
+ private boolean loaded = false;
+
+ public Attribute(String name, T value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public Attribute(String name, T value, boolean readOnly) {
+ this(name, value);
+ this.readOnly = readOnly;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public boolean isLoaded() {
+ return loaded;
+ }
+
+ public void setLoaded(boolean value) {
+ this.loaded = value;
+ }
+
+ /**
+ * Sets the value for this attribute. If the Attribute value is readOnly, a RuntimeException is thrown.
+ *
+ * @param value
+ */
+ public void setValue(T value) {
+ if (readOnly) {
+ throw new RuntimeException("Error setting Attribute value [" + name + " ] - value is read only.");
+ }
+ this.value = value;
+ }
+}
+
+
+
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 0000000..5c37427
--- /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<? extends Serializable> attribute);
+
+ /**
+ * Remove the attribute with given name
+ *
+ * @param name of attribute
+ */
+ void removeAttribute(String name);
+
+
+ // LDAP specific stuff
+ void setEntryDN(String entryDN);
+ String getEntryDN();
+
+
+ /**
+ * Return the attribute value with the specified name
+ *
+ * @param name of attribute
+ * @return attribute value or null if attribute with given name doesn't exist. If given attribute has many values method
+ * will return first one
+ */
+ <T extends Serializable> Attribute<T> getAttribute(String name);
+
+ /**
+ * Returns a Map containing all attribute values for this IdentityType instance.
+ *
+ * @return map of attribute names and their values
+ */
+ Collection<Attribute<? extends Serializable>> getAttributes();
+
+ public final class QUERY_ATTRIBUTE {
+ public static AttributeParameter byName(String name) {
+ return new AttributeParameter(name);
+ }
+ }
+}
+
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 0000000..33b8706
--- /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 {
+
+ /**
+ * <p>Managed properties are stored as ad-hoc attributes and mapped from and to a specific property of a type.</p>
+ *
+ * @return
+ */
+ boolean managed() default false;
+
+}
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 0000000..f8ae8a1
--- /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();
+
+ /**
+ * <p>Sets the current enabled status of this {@link IdentityType}.</p>
+ *
+ * @param enabled
+ */
+ void setEnabled(boolean enabled);
+
+ /**
+ * Returns the date that this IdentityType instance was created.
+ *
+ * @return Date value representing the creation date
+ */
+ Date getCreatedDate();
+
+ /**
+ * <p>Sets the date that this {@link IdentityType} was created.</p>
+ *
+ * @param createdDate
+ */
+ void setCreatedDate(Date createdDate);
+
+ /**
+ * Returns the date that this IdentityType expires, or null if there is no expiry date.
+ *
+ * @return
+ */
+ Date getExpirationDate();
+
+ /**
+ * <p>Sets the date that this {@link IdentityType} expires.</p>
+ *
+ * @param expirationDate
+ */
+ void setExpirationDate(Date expirationDate);
+
+}
+
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 0000000..4ce7ef9
--- /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 0000000..c5feea9
--- /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;
+
+/**
+ * <p>This class can be used to define a query parameter for properties annotated with
+ * {@link org.keycloak.federation.ldap.idm.model.AttributeProperty}.
+ * </p>
+ *
+ * @author pedroigor
+ */
+public class AttributeParameter implements QueryParameter {
+
+ private final String name;
+
+ public AttributeParameter(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+}
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 0000000..85d81d8
--- /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;
+
+/**
+ * <p>A {@link Condition} is used to specify how a specific {@link QueryParameter}
+ * is defined in order to filter query results.</p>
+ *
+ * @author Pedro Igor
+ */
+public interface Condition {
+
+ /**
+ * <p>The {@link QueryParameter} restricted by this condition.</p>
+ *
+ * @return
+ */
+ QueryParameter getParameter();
+
+}
\ 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 0000000..1a77727
--- /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;
+
+/**
+ * <p>An {@link IdentityQuery} is responsible for querying the underlying identity stores for instances of
+ * a given {@link IdentityType}.</p>
+ *
+ * <p>Instances of this class are obtained using the {@link IdentityQueryBuilder#createIdentityQuery(Class)}
+ * method.</p>
+ *
+ * <pre>
+ * IdentityManager identityManager = getIdentityManager();
+ *
+ * // here we get the query builder
+ * IdentityQueryBuilder builder = identityManager.getQueryBuilder();
+ *
+ * // create a condition
+ * Condition condition = builder.equal(User.LOGIN_NAME, "john");
+ *
+ * // create a query for a specific identity type using the previously created condition
+ * IdentityQuery query = builder.createIdentityQuery(User.class).where(condition);
+ *
+ * // execute the query
+ * List<User> result = query.getResultList();
+ * </pre>
+ *
+ * <p>When preparing a query you may want to create conditions to filter its results and configure how they must be retrieved.
+ * For that, you can use the {@link IdentityQueryBuilder}, which provides useful methods for creating
+ * different expressions and conditions.</p>
+ *
+ * @author Shane Bryzak
+ * @author Pedro Igor
+ */
+public interface IdentityQuery<T extends IdentityType> {
+
+ /**
+ * @see #setPaginationContext(Object object)
+ */
+ Object getPaginationContext();
+
+ /**
+ * Used for pagination models like LDAP when search will return some object (like cookie) for searching on next page
+ *
+ * @param object to be used for search next page
+ *
+ * @return this query
+ */
+ IdentityQuery<T> setPaginationContext(Object object);
+
+ /**
+ * @deprecated Will be removed soon.
+ *
+ * @see #setSortParameters(QueryParameter...)
+ */
+ @Deprecated
+ QueryParameter[] getSortParameters();
+
+ /**
+ * Parameters used to sort the results. First parameter has biggest priority. For example: setSortParameter(User.LAST_NAME,
+ * User.FIRST_NAME) means that results will be sorted primarily by lastName and firstName will be used to sort only records with
+ * same lastName
+ *
+ * @param sortParameters parameters to specify sort criteria
+ *
+ * @deprecated Use {@link IdentityQuery#sortBy(Sort...)} instead. Where you can create sort conditions
+ * from the {@link IdentityQueryBuilder}.
+ *
+ * @return this query
+ */
+ @Deprecated
+ IdentityQuery<T> setSortParameters(QueryParameter... sortParameters);
+
+ /**
+ * @deprecated Use {@link IdentityQuery#getSorting()} for a list of sorting conditions. Will be removed soon.
+ *
+ * @return true if sorting will be ascending
+ *
+ * @see #setSortAscending(boolean)
+ */
+ @Deprecated
+ boolean isSortAscending();
+
+ /**
+ * Specify if sorting will be ascending (true) or descending (false)
+ *
+ * @param sortAscending to specify if sorting will be ascending or descending
+ *
+ * @deprecated Use {@link IdentityQuery#sortBy(Sort...)} instead. Where you can create sort conditions
+ * from the {@link IdentityQueryBuilder}.
+ *
+ * @return this query
+ */
+ @Deprecated
+ IdentityQuery<T> setSortAscending(boolean sortAscending);
+
+ /**
+ * <p>Set a query parameter to this query in order to filter the results.</p>
+ *
+ * <p>This method always create an equality condition. For more conditions options take a look at {@link
+ * IdentityQueryBuilder} and use the {@link IdentityQuery#where(Condition...)}
+ * instead.</p>
+ *
+ * @param param The query parameter.
+ * @param value The value to match for equality.
+ *
+ * @return
+ *
+ * @deprecated Use {@link IdentityQuery#where(Condition...)} to specify query conditions.
+ */
+ @Deprecated
+ IdentityQuery<T> setParameter(QueryParameter param, Object... value);
+
+ /**
+ * <p>Add to this query the conditions that will be used to filter results.</p>
+ *
+ * <p>Any condition previously added to this query will be preserved and the new conditions added. If you want to clear the
+ * conditions you must create a new query instance.</p>
+ *
+ * @param condition One or more conditions created from {@link IdentityQueryBuilder}.
+ *
+ * @return
+ */
+ IdentityQuery<T> where(Condition... condition);
+
+ /**
+ * <p>Add to this query the sorting conditions to be applied to the results.</p>
+ *
+ * @param sorts The ordering conditions.
+ *
+ * @return
+ */
+ IdentityQuery<T> sortBy(Sort... sorts);
+
+ /**
+ * <p>The type used to create this query.</p>
+ *
+ * @return
+ */
+ Class<T> getIdentityType();
+
+ /**
+ * <p>Returns a map with all the parameter set for this query.</p>
+ *
+ * @return
+ *
+ * @deprecated Use {@link IdentityQuery#getConditions()} instead. Will be removed.
+ */
+ @Deprecated
+ Map<QueryParameter, Object[]> getParameters();
+
+ /**
+ * <p>Returns a set containing all conditions used by this query to filter its results.</p>
+ *
+ * @return
+ */
+ Set<Condition> getConditions();
+
+ /**
+ * <p>Returns a set containing all sorting conditions used to filter the results.</p>
+ *
+ * @return
+ */
+ Set<Sort> getSorting();
+
+ /**
+ * <p>Returns the value used to restrict the given query parameter.</p>
+ *
+ * @param queryParameter
+ *
+ * @return
+ */
+ @Deprecated
+ Object[] getParameter(QueryParameter queryParameter);
+
+ @Deprecated
+ Map<QueryParameter, Object[]> getParameters(Class<?> type);
+
+ int getOffset();
+
+ /**
+ * <p>Set the position of the first result to retrieve.</p>
+ *
+ * @param offset
+ *
+ * @return
+ */
+ IdentityQuery<T> setOffset(int offset);
+
+ /**
+ * <p>Returns the number of instances to retrieve.</p>
+ *
+ * @return
+ */
+ int getLimit();
+
+ /**
+ * <p>Set the maximum number of results to retrieve.</p>
+ *
+ * @param limit the number of instances to retrieve.
+ *
+ * @return
+ */
+ IdentityQuery<T> setLimit(int limit);
+
+ /**
+ * <p>Execute the query against the underlying identity stores and returns a list containing all instances of
+ * the type (defined when creating this query instance) that match the conditions previously specified.</p>
+ *
+ * @return
+ */
+ List<T> getResultList();
+
+ /**
+ * Count of all query results. It takes into account query parameters, but it doesn't take into account pagination parameter
+ * like offset and limit
+ *
+ * @return count of all query results
+ */
+ int getResultCount();
+}
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 0000000..0220635
--- /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;
+
+/**
+ * <p>The {@link IdentityQueryBuilder} is responsible for creating {@link IdentityQuery} instances and also
+ * provide methods to create conditions, orderings, sorting, etc.</p>
+ *
+ * @author Pedro Igor
+ */
+public interface IdentityQueryBuilder {
+
+ /**
+ * <p>Create a condition for testing the whether the query parameter satisfies the given pattern..</p>
+ *
+ * @param parameter The query parameter.
+ * @param pattern The pattern to match.
+ *
+ * @return
+ */
+ Condition like(QueryParameter parameter, String pattern);
+
+ /**
+ * <p>Create a condition for testing the arguments for equality.</p>
+ *
+ * @param parameter The query parameter.
+ * @param value The value to compare.
+ *
+ * @return
+ */
+ Condition equal(QueryParameter parameter, Object value);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is grater than the given value..</p>
+ *
+ * @param parameter The query parameter.
+ * @param x The value to compare.
+ *
+ * @return
+ */
+ Condition greaterThan(QueryParameter parameter, Object x);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is grater than or equal to the given value..</p>
+ *
+ * @param parameter The query parameter.
+ * @param x The value to compare.
+ *
+ * @return
+ */
+ Condition greaterThanOrEqualTo(QueryParameter parameter, Object x);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is less than the given value..</p>
+ *
+ * @param parameter The query parameter.
+ * @param x The value to compare.
+ *
+ * @return
+ */
+ Condition lessThan(QueryParameter parameter, Object x);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is less than or equal to the given value..</p>
+ *
+ * @param parameter The query parameter.
+ * @param x The value to compare.
+ *
+ * @return
+ */
+ Condition lessThanOrEqualTo(QueryParameter parameter, Object x);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is between the given values.</p>
+ *
+ * @param parameter The query parameter.
+ * @param x The first value.
+ * @param x The second value.
+ *
+ * @return
+ */
+ Condition between(QueryParameter parameter, Object x, Object y);
+
+ /**
+ * <p>Create a condition for testing whether the query parameter is contained in a list of values.</p>
+ *
+ * @param parameter The query parameter.
+ * @param values A list of values.
+ *
+ * @return
+ */
+ Condition in(QueryParameter parameter, Object... values);
+
+ /**
+ * <p>Create an ascending order for the given <code>parameter</code>. Once created, you can use it to sort the results of a
+ * query.</p>
+ *
+ * @param parameter The query parameter to sort.
+ *
+ * @return
+ */
+ Sort asc(QueryParameter parameter);
+
+ /**
+ * <p>Create an descending order for the given <code>parameter</code>. Once created, you can use it to sort the results of a
+ * query.</p>
+ *
+ * @param parameter The query parameter to sort.
+ *
+ * @return
+ */
+ Sort desc(QueryParameter parameter);
+
+ /**
+ * <p> Create an {@link IdentityQuery} that can be used to query for {@link
+ * IdentityType} instances of a the given <code>identityType</code>. </p>
+ *
+ * @param identityType The type to search. If you provide the {@link IdentityType}
+ * base interface any of its sub-types will be returned.
+ *
+ * @return
+ */
+ <T extends IdentityType> IdentityQuery<T> createIdentityQuery(Class<T> identityType);
+}
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 0000000..672fdaa
--- /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 0000000..21b0253
--- /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 <T>
+ *
+ * @author Shane Bryzak
+ */
+public class DefaultIdentityQuery<T extends IdentityType> implements IdentityQuery<T> {
+
+ private final Map<QueryParameter, Object[]> parameters = new LinkedHashMap<QueryParameter, Object[]>();
+ private final Class<T> identityType;
+ private final IdentityStore identityStore;
+ private final IdentityQueryBuilder queryBuilder;
+ private int offset;
+ private int limit;
+ private Object paginationContext;
+ private QueryParameter[] sortParameters;
+ private boolean sortAscending = true;
+ private final Set<Condition> conditions = new LinkedHashSet<Condition>();
+ private final Set<Sort> ordering = new LinkedHashSet<Sort>();
+
+ public DefaultIdentityQuery(IdentityQueryBuilder queryBuilder, Class<T> identityType, IdentityStore identityStore) {
+ this.queryBuilder = queryBuilder;
+ this.identityStore = identityStore;
+ this.identityType = identityType;
+ }
+
+ @Override
+ public IdentityQuery<T> setParameter(QueryParameter queryParameter, Object... value) {
+ if (value == null || value.length == 0) {
+ throw new ModelException("Query Parameter values null or empty");
+ }
+
+ parameters.put(queryParameter, value);
+
+ if (IdentityType.CREATED_AFTER.equals(queryParameter) || IdentityType.EXPIRY_AFTER.equals(queryParameter)) {
+ this.conditions.add(queryBuilder.greaterThanOrEqualTo(queryParameter, value[0]));
+ } else if (IdentityType.CREATED_BEFORE.equals(queryParameter) || IdentityType.EXPIRY_BEFORE.equals(queryParameter)) {
+ this.conditions.add(queryBuilder.lessThanOrEqualTo(queryParameter, value[0]));
+ } else {
+ this.conditions.add(queryBuilder.equal(queryParameter, value[0]));
+ }
+
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> where(Condition... condition) {
+ this.conditions.addAll(Arrays.asList(condition));
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> sortBy(Sort... sorts) {
+ this.ordering.addAll(Arrays.asList(sorts));
+ return this;
+ }
+
+ @Override
+ public Set<Sort> getSorting() {
+ return unmodifiableSet(this.ordering);
+ }
+
+ @Override
+ public Class<T> getIdentityType() {
+ return identityType;
+ }
+
+ @Override
+ public Map<QueryParameter, Object[]> getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public Object[] getParameter(QueryParameter queryParameter) {
+ return this.parameters.get(queryParameter);
+ }
+
+ @Override
+ public Map<QueryParameter, Object[]> getParameters(Class<?> type) {
+ Map<QueryParameter, Object[]> typedParameters = new HashMap<QueryParameter, Object[]>();
+
+ Set<Map.Entry<QueryParameter, Object[]>> entrySet = this.parameters.entrySet();
+
+ for (Map.Entry<QueryParameter, Object[]> entry : entrySet) {
+ if (type.isInstance(entry.getKey())) {
+ typedParameters.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ return typedParameters;
+ }
+
+ @Override
+ public int getLimit() {
+ return limit;
+ }
+
+ @Override
+ public int getOffset() {
+ return offset;
+ }
+
+ @Override
+ public Object getPaginationContext() {
+ return paginationContext;
+ }
+
+ @Override
+ public QueryParameter[] getSortParameters() {
+ return sortParameters;
+ }
+
+ @Override
+ public boolean isSortAscending() {
+ return sortAscending;
+ }
+
+ @Override
+ public List<T> getResultList() {
+
+ // remove this statement once deprecated methods on IdentityQuery are removed
+ if (this.sortParameters != null) {
+ for (QueryParameter parameter : this.sortParameters) {
+ if (isSortAscending()) {
+ sortBy(this.queryBuilder.asc(parameter));
+ } else {
+ sortBy(this.queryBuilder.desc(parameter));
+ }
+ }
+ }
+
+ List<T> result = new ArrayList<T>();
+
+ try {
+ for (T identityType : identityStore.fetchQueryResults(this)) {
+ result.add(identityType);
+ }
+ } catch (Exception e) {
+ throw new ModelException("LDAP Query failed", e);
+ }
+
+ return result;
+ }
+
+ @Override
+ public int getResultCount() {
+ return identityStore.countQueryResults(this);
+ }
+
+ @Override
+ public IdentityQuery<T> setOffset(int offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> setLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> setSortParameters(QueryParameter... sortParameters) {
+ this.sortParameters = sortParameters;
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> setSortAscending(boolean sortAscending) {
+ this.sortAscending = sortAscending;
+ return this;
+ }
+
+ @Override
+ public IdentityQuery<T> setPaginationContext(Object object) {
+ this.paginationContext = object;
+ return this;
+ }
+
+ @Override
+ public Set<Condition> getConditions() {
+ return unmodifiableSet(this.conditions);
+ }
+}
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 0000000..5d3b72e
--- /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 <T extends IdentityType> IdentityQuery createIdentityQuery(Class<T> identityType) {
+ return new DefaultIdentityQuery(this, identityType, this.identityStore);
+ }
+
+ private void throwExceptionIfNotComparable(Object x) {
+ if (!Comparable.class.isInstance(x)) {
+ throw new ModelException("Query parameter value [" + x + "] must be " + Comparable.class + ".");
+ }
+ }
+}
\ 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 0000000..a3fee26
--- /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 0000000..cbdf540
--- /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 0000000..54d2234
--- /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 0000000..5906a5c
--- /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 0000000..6c68103
--- /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/query/QueryParameter.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/QueryParameter.java
new file mode 100644
index 0000000..ae2bbdf
--- /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 0000000..dfd331e
--- /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/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
new file mode 100644
index 0000000..7fef705
--- /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
+
+ <V extends IdentityType> List<V> fetchQueryResults(IdentityQuery<V> identityQuery);
+
+ <V extends IdentityType> int countQueryResults(IdentityQuery<V> identityQuery);
+
+// // Relationship query
+//
+// <V extends Relationship> List<V> fetchQueryResults(RelationshipQuery<V> query);
+//
+// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);
+
+ // Credentials
+
+ /**
+ * Validates the specified credentials.
+ *
+ * @param user Keycloak user
+ * @param password Ldap password
+ */
+ boolean validatePassword(LDAPUser user, String password);
+
+ /**
+ * Updates the specified credential value.
+ *
+ * @param user Keycloak user
+ * @param password Ldap password
+ */
+ void updatePassword(LDAPUser user, String password);
+
+}
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 0000000..8b91240
--- /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 <a href="mailto:psilva@redhat.com">Pedro Silva</a>
+ */
+public class LDAPIdentityStore implements IdentityStore {
+
+ private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
+
+ public static final String EMPTY_ATTRIBUTE_VALUE = " ";
+
+ private final LDAPIdentityStoreConfiguration config;
+ private final LDAPOperationManager operationManager;
+
+ public LDAPIdentityStore(LDAPIdentityStoreConfiguration config) {
+ this.config = config;
+
+ try {
+ this.operationManager = new LDAPOperationManager(getConfig());
+ } catch (NamingException e) {
+ throw new ModelException("Couldn't init operation manager", e);
+ }
+ }
+
+ @Override
+ public LDAPIdentityStoreConfiguration getConfig() {
+ return this.config;
+ }
+
+ @Override
+ public void add(AttributedType attributedType) {
+ // id will be assigned by the ldap server
+ attributedType.setId(null);
+
+ String entryDN = getBindingDN(attributedType, true);
+ this.operationManager.createSubContext(entryDN, extractAttributes(attributedType, true));
+ addToParentAsMember(attributedType);
+ attributedType.setId(getEntryIdentifier(attributedType));
+
+ attributedType.setEntryDN(entryDN);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Type with identifier [%s] successfully added to identity store [%s].", attributedType.getId(), this);
+ }
+ }
+
+ @Override
+ public void update(AttributedType attributedType) {
+ BasicAttributes updatedAttributes = extractAttributes(attributedType, false);
+ NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
+
+ this.operationManager.modifyAttributes(getBindingDN(attributedType, true), attributes);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Type with identifier [%s] successfully updated to identity store [%s].", attributedType.getId(), this);
+ }
+ }
+
+ @Override
+ public void remove(AttributedType attributedType) {
+ LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
+
+ this.operationManager.removeEntryById(getBaseDN(attributedType), attributedType.getId(), mappingConfig);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Type with identifier [%s] successfully removed from identity store [%s].", attributedType.getId(), this);
+ }
+ }
+
+ @Override
+ public <V extends IdentityType> List<V> fetchQueryResults(IdentityQuery<V> identityQuery) {
+ List<V> results = new ArrayList<V>();
+
+ try {
+ if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
+ throw new ModelException("LDAP Identity Store does not support sorted queries.");
+ }
+
+ for (Condition condition : identityQuery.getConditions()) {
+
+ if (IdentityType.ID.equals(condition.getParameter())) {
+ if (EqualCondition.class.isInstance(condition)) {
+ EqualCondition equalCondition = (EqualCondition) condition;
+ SearchResult search = this.operationManager
+ .lookupById(getConfig().getBaseDN(), equalCondition.getValue().toString(), null);
+
+ if (search != null) {
+ results.add((V) populateAttributedType(search, null));
+ }
+ }
+
+ return results;
+ }
+ }
+
+ if (!IdentityType.class.equals(identityQuery.getIdentityType())) {
+ // the ldap store does not support queries based on root types. Except if based on the identifier.
+ LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(identityQuery.getIdentityType());
+ StringBuilder filter = createIdentityTypeSearchFilter(identityQuery, ldapEntryConfig);
+ String baseDN = getBaseDN(ldapEntryConfig);
+ List<SearchResult> search;
+
+ if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
+ search = this.operationManager.searchPaginated(baseDN, filter.toString(), ldapEntryConfig, identityQuery);
+ } else {
+ search = this.operationManager.search(baseDN, filter.toString(), ldapEntryConfig);
+ }
+
+ for (SearchResult result : search) {
+ if (!result.getNameInNamespace().equals(baseDN)) {
+ results.add((V) populateAttributedType(result, null));
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new ModelException("Querying of identity type failed " + identityQuery, e);
+ }
+
+ return results;
+ }
+
+ @Override
+ public <V extends IdentityType> int countQueryResults(IdentityQuery<V> identityQuery) {
+ int limit = identityQuery.getLimit();
+ int offset = identityQuery.getOffset();
+
+ identityQuery.setLimit(0);
+ identityQuery.setOffset(0);
+
+ int resultCount = identityQuery.getResultList().size();
+
+ identityQuery.setLimit(limit);
+ identityQuery.setOffset(offset);
+
+ return resultCount;
+ }
+
+ public IdentityQueryBuilder createQueryBuilder() {
+ return new DefaultQueryBuilder(this);
+ }
+
+ // *************** CREDENTIALS AND USER SPECIFIC STUFF
+
+ @Override
+ public boolean validatePassword(LDAPUser user, String password) {
+ String userDN = getEntryDNOfUser(user);
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Using DN [%s] for authentication of user [%s]", userDN, user.getLoginName());
+ }
+
+ if (operationManager.authenticate(userDN, password)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void updatePassword(LDAPUser user, String password) {
+ String userDN = getEntryDNOfUser(user);
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Using DN [%s] for updating LDAP password of user [%s]", userDN, user.getLoginName());
+ }
+
+ if (getConfig().isActiveDirectory()) {
+ updateADPassword(userDN, password);
+ } else {
+ ModificationItem[] mods = new ModificationItem[1];
+
+ try {
+ BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
+
+ mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
+
+ operationManager.modifyAttribute(userDN, mod0);
+ } catch (Exception e) {
+ throw new ModelException("Error updating password.", e);
+ }
+ }
+ }
+
+
+ private void updateADPassword(String userDN, String password) {
+ try {
+ // Replace the "unicdodePwd" attribute with a new value
+ // Password must be both Unicode and a quoted string
+ String newQuotedPassword = "\"" + password + "\"";
+ byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
+
+ BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);
+
+ List<ModificationItem> modItems = new ArrayList<ModificationItem>();
+ modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
+
+ // Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
+ if (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
+ BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
+ modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));
+
+ logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
+ }
+
+ operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
+ } catch (Exception e) {
+ throw new ModelException(e);
+ }
+ }
+
+
+ private String getEntryDNOfUser(LDAPUser user) {
+ // First try if user already has entryDN on him
+ String entryDN = user.getEntryDN();
+ if (entryDN != null) {
+ return entryDN;
+ }
+
+ // Need to find user in LDAP
+ String username = user.getLoginName();
+ user = getUser(username);
+ if (user == null) {
+ throw new ModelException("No LDAP user found with username " + username);
+ }
+
+ return user.getEntryDN();
+ }
+
+
+ public LDAPUser getUser(String username) {
+
+ if (isNullOrEmpty(username)) {
+ return null;
+ }
+
+ IdentityQueryBuilder queryBuilder = createQueryBuilder();
+ List<LDAPUser> agents = queryBuilder.createIdentityQuery(LDAPUser.class)
+ .where(queryBuilder.equal(LDAPUser.LOGIN_NAME, username)).getResultList();
+
+ if (agents.isEmpty()) {
+ return null;
+ } else if (agents.size() == 1) {
+ return agents.get(0);
+ } else {
+ throw new ModelDuplicateException("Error - multiple Agent objects found with same login name");
+ }
+ }
+
+ // ************ END CREDENTIALS AND USER SPECIFIC STUFF
+
+
+ private String getBaseDN(final LDAPMappingConfiguration ldapEntryConfig) {
+ String baseDN = getConfig().getBaseDN();
+
+ if (ldapEntryConfig.getBaseDN() != null) {
+ baseDN = ldapEntryConfig.getBaseDN();
+ }
+
+ return baseDN;
+ }
+
+ protected <V extends IdentityType> StringBuilder createIdentityTypeSearchFilter(final IdentityQuery<V> identityQuery, final LDAPMappingConfiguration ldapEntryConfig) {
+ StringBuilder filter = new StringBuilder();
+
+ for (Condition condition : identityQuery.getConditions()) {
+ QueryParameter queryParameter = condition.getParameter();
+
+ if (!IdentityType.ID.equals(queryParameter)) {
+ if (AttributeParameter.class.isInstance(queryParameter)) {
+ AttributeParameter attributeParameter = (AttributeParameter) queryParameter;
+ String attributeName = ldapEntryConfig.getMappedProperties().get(attributeParameter.getName());
+
+ if (attributeName != null) {
+ if (EqualCondition.class.isInstance(condition)) {
+ EqualCondition equalCondition = (EqualCondition) condition;
+ Object parameterValue = equalCondition.getValue();
+
+ if (Date.class.isInstance(parameterValue)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(parameterValue).append(")");
+ } else if (LikeCondition.class.isInstance(condition)) {
+ LikeCondition likeCondition = (LikeCondition) condition;
+ String parameterValue = (String) likeCondition.getValue();
+
+ } else if (GreaterThanCondition.class.isInstance(condition)) {
+ GreaterThanCondition greaterThanCondition = (GreaterThanCondition) condition;
+ Comparable parameterValue = (Comparable) greaterThanCondition.getValue();
+
+ if (Date.class.isInstance(parameterValue)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ if (greaterThanCondition.isOrEqual()) {
+ filter.append("(").append(attributeName).append(">=").append(parameterValue).append(")");
+ } else {
+ filter.append("(").append(attributeName).append(">").append(parameterValue).append(")");
+ }
+ } else if (LessThanCondition.class.isInstance(condition)) {
+ LessThanCondition lessThanCondition = (LessThanCondition) condition;
+ Comparable parameterValue = (Comparable) lessThanCondition.getValue();
+
+ if (Date.class.isInstance(parameterValue)) {
+ parameterValue = LDAPUtil.formatDate((Date) parameterValue);
+ }
+
+ if (lessThanCondition.isOrEqual()) {
+ filter.append("(").append(attributeName).append("<=").append(parameterValue).append(")");
+ } else {
+ filter.append("(").append(attributeName).append("<").append(parameterValue).append(")");
+ }
+ } else if (BetweenCondition.class.isInstance(condition)) {
+ BetweenCondition betweenCondition = (BetweenCondition) condition;
+ Comparable x = betweenCondition.getX();
+ Comparable y = betweenCondition.getY();
+
+ if (Date.class.isInstance(x)) {
+ x = LDAPUtil.formatDate((Date) x);
+ }
+
+ if (Date.class.isInstance(y)) {
+ y = LDAPUtil.formatDate((Date) y);
+ }
+
+ filter.append("(").append(x).append("<=").append(attributeName).append("<=").append(y).append(")");
+ } else if (InCondition.class.isInstance(condition)) {
+ InCondition inCondition = (InCondition) condition;
+ Object[] valuesToCompare = inCondition.getValue();
+
+ filter.append("(&(");
+
+ for (int i = 0; i< valuesToCompare.length; i++) {
+ Object value = valuesToCompare[i];
+
+ filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(value).append(")");
+ }
+
+ filter.append("))");
+ } else {
+ throw new ModelException("Unsupported query condition [" + condition + "].");
+ }
+ }
+ }
+ }
+ }
+
+
+ filter.insert(0, "(&(");
+ filter.append(getObjectClassesFilter(ldapEntryConfig));
+ filter.append("))");
+
+ return filter;
+ }
+
+ private StringBuilder getObjectClassesFilter(final LDAPMappingConfiguration ldapEntryConfig) {
+ StringBuilder builder = new StringBuilder();
+
+ if (ldapEntryConfig != null && !ldapEntryConfig.getObjectClasses().isEmpty()) {
+ for (String objectClass : ldapEntryConfig.getObjectClasses()) {
+ builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
+ }
+ } else {
+ builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
+ }
+
+ return builder;
+ }
+
+ private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType) {
+ return populateAttributedType(searchResult, attributedType, 0);
+ }
+
+ private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType, int hierarchyDepthCount) {
+ try {
+ String entryDN = searchResult.getNameInNamespace();
+ Attributes attributes = searchResult.getAttributes();
+
+ if (attributedType == null) {
+ attributedType = Reflections.newInstance(getConfig().getSupportedTypeByBaseDN(entryDN, getEntryObjectClasses(attributes)));
+ }
+
+ attributedType.setEntryDN(entryDN);
+
+ LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
+
+ if (hierarchyDepthCount > mappingConfig.getHierarchySearchDepth()) {
+ return null;
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Populating attributed type [%s] from DN [%s]", attributedType, entryDN);
+ }
+
+ NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
+
+ while (ldapAttributes.hasMore()) {
+ Attribute ldapAttribute = ldapAttributes.next();
+ Object attributeValue;
+
+ try {
+ attributeValue = ldapAttribute.get();
+ } catch (NoSuchElementException nsee) {
+ continue;
+ }
+
+ String ldapAttributeName = ldapAttribute.getID();
+
+ if (ldapAttributeName.toLowerCase().equals(getConfig().getUniqueIdentifierAttributeName().toLowerCase())) {
+ attributedType.setId(this.operationManager.decodeEntryUUID(attributeValue));
+ } else {
+ String attributeName = findAttributeName(mappingConfig.getMappedProperties(), ldapAttributeName);
+
+ if (attributeName != null) {
+ // Find if it's java property or attribute
+ Property<Object> property = PropertyQueries
+ .createQuery(attributedType.getClass())
+ .addCriteria(new NamedPropertyCriteria(attributeName)).getFirstResult();
+
+ if (property != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Populating property [%s] from ldap attribute [%s] with value [%s] from DN [%s].", property.getName(), ldapAttributeName, attributeValue, entryDN);
+ }
+
+ if (property.getJavaClass().equals(Date.class)) {
+ property.setValue(attributedType, LDAPUtil.parseDate(attributeValue.toString()));
+ } else {
+ property.setValue(attributedType, attributeValue);
+ }
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Populating attribute [%s] from ldap attribute [%s] with value [%s] from DN [%s].", attributeName, ldapAttributeName, attributeValue, entryDN);
+ }
+
+ attributedType.setAttribute(new org.keycloak.federation.ldap.idm.model.Attribute(attributeName, (Serializable) attributeValue));
+ }
+ }
+ }
+ }
+
+ if (IdentityType.class.isInstance(attributedType)) {
+ IdentityType identityType = (IdentityType) attributedType;
+
+ String createdTimestamp = attributes.get(LDAPConstants.CREATE_TIMESTAMP).get().toString();
+
+ identityType.setCreatedDate(LDAPUtil.parseDate(createdTimestamp));
+ }
+
+ LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());
+
+ if (mappingConfig.getParentMembershipAttributeName() != null) {
+ StringBuilder filter = new StringBuilder("(&");
+ String entryBaseDN = entryDN.substring(entryDN.indexOf(LDAPConstants.COMMA) + 1);
+
+ filter
+ .append("(")
+ .append(getObjectClassesFilter(entryConfig))
+ .append(")")
+ .append("(")
+ .append(mappingConfig.getParentMembershipAttributeName())
+ .append(LDAPConstants.EQUAL).append("")
+ .append(getBindingDN(attributedType, false))
+ .append(LDAPConstants.COMMA)
+ .append(entryBaseDN)
+ .append(")");
+
+ filter.append(")");
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Searching parent entry for DN [%s] using filter [%s].", entryBaseDN, filter.toString());
+ }
+
+ List<SearchResult> search = this.operationManager.search(getConfig().getBaseDN(), filter.toString(), entryConfig);
+
+ if (!search.isEmpty()) {
+ SearchResult next = search.get(0);
+
+ Property<AttributedType> parentProperty = PropertyQueries
+ .<AttributedType>createQuery(attributedType.getClass())
+ .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();
+
+ if (parentProperty != null) {
+ String parentDN = next.getNameInNamespace();
+ String parentBaseDN = parentDN.substring(parentDN.indexOf(",") + 1);
+ Class<? extends AttributedType> baseDNType = getConfig().getSupportedTypeByBaseDN(parentBaseDN, getEntryObjectClasses(attributes));
+
+ if (parentProperty.getJavaClass().isAssignableFrom(baseDNType)) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Found parent [%s] for entry for DN [%s].", parentDN, entryDN);
+ }
+
+ int hierarchyDepthCount1 = ++hierarchyDepthCount;
+
+ parentProperty.setValue(attributedType, populateAttributedType(next, null, hierarchyDepthCount1));
+ }
+ }
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("No parent entry found for DN [%s] using filter [%s].", entryDN, filter.toString());
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new ModelException("Could not populate attribute type " + attributedType + ".", e);
+ }
+
+ return attributedType;
+ }
+
+ private String findAttributeName(Map<String, String> attrMapping, String ldapAttributeName) {
+ for (Map.Entry<String,String> currentAttr : attrMapping.entrySet()) {
+ if (currentAttr.getValue().equalsIgnoreCase(ldapAttributeName)) {
+ return currentAttr.getKey();
+ }
+ }
+
+ return null;
+ }
+
+ private List<String> getEntryObjectClasses(final Attributes attributes) throws NamingException {
+ Attribute objectClassesAttribute = attributes.get(LDAPConstants.OBJECT_CLASS);
+ List<String> objectClasses = new ArrayList<String>();
+
+ if (objectClassesAttribute == null) {
+ return objectClasses;
+ }
+
+ NamingEnumeration<?> all = objectClassesAttribute.getAll();
+
+ while (all.hasMore()) {
+ objectClasses.add(all.next().toString());
+ }
+
+ return objectClasses;
+ }
+
+ protected BasicAttributes extractAttributes(AttributedType attributedType, boolean isCreate) {
+ BasicAttributes entryAttributes = new BasicAttributes();
+ LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
+ Map<String, String> mappedProperties = mappingConfig.getMappedProperties();
+
+ for (String propertyName : mappedProperties.keySet()) {
+ if (!mappingConfig.getReadOnlyAttributes().contains(propertyName) && (isCreate || !mappingConfig.getBindingProperty().getName().equals(propertyName))) {
+ Property<Object> property = PropertyQueries
+ .<Object>createQuery(attributedType.getClass())
+ .addCriteria(new NamedPropertyCriteria(propertyName)).getFirstResult();
+
+ Object propertyValue = null;
+ if (property != null) {
+ // Mapped Java property on the object
+ propertyValue = property.getValue(attributedType);
+ } else {
+ // Not mapped property. So fallback to attribute
+ org.keycloak.federation.ldap.idm.model.Attribute<?> attribute = attributedType.getAttribute(propertyName);
+ if (attribute != null) {
+ propertyValue = attribute.getValue();
+ }
+ }
+
+ if (AttributedType.class.isInstance(propertyValue)) {
+ AttributedType referencedType = (AttributedType) propertyValue;
+ propertyValue = getBindingDN(referencedType, true);
+ } else {
+ if (propertyValue == null || isNullOrEmpty(propertyValue.toString())) {
+ propertyValue = EMPTY_ATTRIBUTE_VALUE;
+ }
+ }
+
+ entryAttributes.put(mappedProperties.get(propertyName), propertyValue);
+ }
+ }
+
+ // Don't extract object classes for update
+ if (isCreate) {
+ LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(attributedType.getClass());
+
+ BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);
+
+ for (String objectClassValue : ldapEntryConfig.getObjectClasses()) {
+ objectClassAttribute.add(objectClassValue);
+
+ if (objectClassValue.equals(LDAPConstants.GROUP_OF_NAMES)
+ || objectClassValue.equals(LDAPConstants.GROUP_OF_ENTRIES)
+ || objectClassValue.equals(LDAPConstants.GROUP_OF_UNIQUE_NAMES)) {
+ entryAttributes.put(LDAPConstants.MEMBER, EMPTY_ATTRIBUTE_VALUE);
+ }
+ }
+
+ entryAttributes.put(objectClassAttribute);
+ }
+
+ return entryAttributes;
+ }
+
+ // TODO: Move class StringUtil from SAML module
+ public static boolean isNullOrEmpty(String str) {
+ return str == null || str.isEmpty();
+ }
+
+ private LDAPMappingConfiguration getMappingConfig(Class<? extends AttributedType> attributedType) {
+ LDAPMappingConfiguration mappingConfig = getConfig().getMappingConfig(attributedType);
+
+ if (mappingConfig == null) {
+ throw new ModelException("Not mapped type [" + attributedType + "].");
+ }
+
+ return mappingConfig;
+ }
+
+ public String getBindingDN(AttributedType attributedType, boolean appendBaseDN) {
+ LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
+ Property<String> idProperty = mappingConfig.getIdProperty();
+
+ String baseDN;
+
+ if (mappingConfig.getBaseDN() == null || !appendBaseDN) {
+ baseDN = "";
+ } else {
+ baseDN = LDAPConstants.COMMA + getBaseDN(attributedType);
+ }
+
+ Property<String> bindingProperty = mappingConfig.getBindingProperty();
+ String bindingAttribute;
+ String dn;
+
+ if (bindingProperty == null) {
+ bindingAttribute = mappingConfig.getMappedProperties().get(idProperty.getName());
+ dn = idProperty.getValue(attributedType);
+ } else {
+ bindingAttribute = mappingConfig.getMappedProperties().get(bindingProperty.getName());
+ dn = mappingConfig.getBindingProperty().getValue(attributedType);
+ }
+
+ return bindingAttribute + LDAPConstants.EQUAL + dn + baseDN;
+ }
+
+ private String getBaseDN(AttributedType attributedType) {
+ LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
+ String baseDN = mappingConfig.getBaseDN();
+ String parentDN = mappingConfig.getParentMapping().get(mappingConfig.getIdProperty().getValue(attributedType));
+
+ if (parentDN != null) {
+ baseDN = parentDN;
+ } else {
+ Property<AttributedType> parentProperty = PropertyQueries
+ .<AttributedType>createQuery(attributedType.getClass())
+ .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();
+
+ if (parentProperty != null) {
+ AttributedType parentType = parentProperty.getValue(attributedType);
+
+ if (parentType != null) {
+ Property<String> parentIdProperty = getMappingConfig(parentType.getClass()).getIdProperty();
+
+ String parentId = parentIdProperty.getValue(parentType);
+
+ String parentBaseDN = mappingConfig.getParentMapping().get(parentId);
+
+ if (parentBaseDN != null) {
+ baseDN = parentBaseDN;
+ } else {
+ baseDN = getBaseDN(parentType);
+ }
+ }
+ }
+ }
+
+ if (baseDN == null) {
+ baseDN = getConfig().getBaseDN();
+ }
+
+ return baseDN;
+ }
+
+ protected void addToParentAsMember(final AttributedType attributedType) {
+ LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());
+
+ if (entryConfig.getParentMembershipAttributeName() != null) {
+ Property<AttributedType> parentProperty = PropertyQueries
+ .<AttributedType>createQuery(attributedType.getClass())
+ .addCriteria(new TypedPropertyCriteria(attributedType.getClass()))
+ .getFirstResult();
+
+ if (parentProperty != null) {
+ AttributedType parentType = parentProperty.getValue(attributedType);
+
+ if (parentType != null) {
+ Attributes attributes = this.operationManager.getAttributes(parentType.getId(), getBaseDN(parentType), entryConfig);
+ Attribute attribute = attributes.get(entryConfig.getParentMembershipAttributeName());
+
+ attribute.add(getBindingDN(attributedType, true));
+
+ this.operationManager.modifyAttribute(getBindingDN(parentType, true), attribute);
+ }
+ }
+ }
+ }
+
+ protected String getEntryIdentifier(final AttributedType attributedType) {
+ try {
+ // we need this to retrieve the entry's identifier from the ldap server
+ List<SearchResult> search = this.operationManager.search(getBaseDN(attributedType), "(" + getBindingDN(attributedType, false) + ")", getMappingConfig(attributedType.getClass()));
+ Attribute id = search.get(0).getAttributes().get(getConfig().getUniqueIdentifierAttributeName());
+
+ if (id == null) {
+ throw new ModelException("Could not retrieve identifier for entry [" + getBindingDN(attributedType, true) + "].");
+ }
+
+ return this.operationManager.decodeEntryUUID(id.get());
+ } catch (NamingException ne) {
+ throw new ModelException("Could not add type [" + attributedType + "].", ne);
+ }
+ }
+}
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 0000000..0c0a2e1
--- /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<Class<? extends AttributedType>, LDAPMappingConfiguration> mappingConfig = new HashMap<Class<? extends AttributedType>, LDAPMappingConfiguration>();
+
+ public String getLdapURL() {
+ return this.ldapURL;
+ }
+
+ public String getFactoryName() {
+ return this.factoryName;
+ }
+
+ public String getAuthType() {
+ return this.authType;
+ }
+
+ public String getBaseDN() {
+ return this.baseDN;
+ }
+
+ public String getBindDN() {
+ return this.bindDN;
+ }
+
+ public String getBindCredential() {
+ return this.bindCredential;
+ }
+
+ public boolean isActiveDirectory() {
+ return this.activeDirectory;
+ }
+
+ public Properties getConnectionProperties() {
+ return this.connectionProperties;
+ }
+
+ public LDAPMappingConfiguration mappingConfig(Class<? extends AttributedType> clazz) {
+ LDAPMappingConfiguration mappingConfig = new LDAPMappingConfiguration(clazz);
+ this.mappingConfig.put(clazz, mappingConfig);
+ return mappingConfig;
+ }
+
+ public Class<? extends AttributedType> getSupportedTypeByBaseDN(String entryDN, List<String> objectClasses) {
+ String entryBaseDN = entryDN.substring(entryDN.indexOf(LDAPConstants.COMMA) + 1);
+
+ for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
+ if (mappingConfig.getBaseDN() != null) {
+
+ if (mappingConfig.getBaseDN().equalsIgnoreCase(entryDN)
+ || mappingConfig.getParentMapping().values().contains(entryDN)) {
+ return mappingConfig.getMappedClass();
+ }
+
+ if (mappingConfig.getBaseDN().equalsIgnoreCase(entryBaseDN)
+ || mappingConfig.getParentMapping().values().contains(entryBaseDN)) {
+ return mappingConfig.getMappedClass();
+ }
+ }
+ }
+
+ for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
+ for (String objectClass : objectClasses) {
+ if (mappingConfig.getObjectClasses().contains(objectClass)) {
+ return mappingConfig.getMappedClass();
+ }
+ }
+ }
+
+ throw new ModelException("No type found with Base DN [" + entryDN + "] or objectClasses [" + objectClasses + ".");
+ }
+
+ public LDAPMappingConfiguration getMappingConfig(Class<? extends AttributedType> attributedType) {
+ for (LDAPMappingConfiguration mappingConfig : this.mappingConfig.values()) {
+ if (attributedType.equals(mappingConfig.getMappedClass())) {
+ return mappingConfig;
+ }
+ }
+
+ return null;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public String getUniqueIdentifierAttributeName() {
+ return uniqueIdentifierAttributeName;
+ }
+
+ public boolean isPagination() {
+ return pagination;
+ }
+
+ public boolean isUserAccountControlsAfterPasswordUpdate() {
+ return userAccountControlsAfterPasswordUpdate;
+ }
+
+ public LDAPIdentityStoreConfiguration setLdapURL(String ldapURL) {
+ this.ldapURL = ldapURL;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setFactoryName(String factoryName) {
+ this.factoryName = factoryName;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setAuthType(String authType) {
+ this.authType = authType;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setProtocol(String protocol) {
+ this.protocol = protocol;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setBindDN(String bindDN) {
+ this.bindDN = bindDN;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setBindCredential(String bindCredential) {
+ this.bindCredential = bindCredential;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setActiveDirectory(boolean activeDirectory) {
+ this.activeDirectory = activeDirectory;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setPagination(boolean pagination) {
+ this.pagination = pagination;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setConnectionProperties(Properties connectionProperties) {
+ this.connectionProperties = connectionProperties;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setUniqueIdentifierAttributeName(String uniqueIdentifierAttributeName) {
+ this.uniqueIdentifierAttributeName = uniqueIdentifierAttributeName;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setUserAccountControlsAfterPasswordUpdate(boolean userAccountControlsAfterPasswordUpdate) {
+ this.userAccountControlsAfterPasswordUpdate = userAccountControlsAfterPasswordUpdate;
+ return this;
+ }
+
+ public LDAPIdentityStoreConfiguration setBaseDN(String baseDN) {
+ this.baseDN = baseDN;
+ return this;
+ }
+}
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 0000000..033d93b
--- /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<? extends AttributedType> mappedClass;
+ private Set<String> objectClasses;
+ private String baseDN;
+ private final Map<String, String> mappedProperties = new HashMap<String, String>();
+ private Property<String> idProperty;
+ private Class<? extends AttributedType> relatedAttributedType;
+ private String parentMembershipAttributeName;
+ private Map<String, String> parentMapping = new HashMap<String, String>();
+ private final Set<String> readOnlyAttributes = new HashSet<String>();
+ private int hierarchySearchDepth;
+ private Property<String> bindingProperty;
+
+ public LDAPMappingConfiguration(Class<? extends AttributedType> mappedClass) {
+ this.mappedClass = mappedClass;
+ }
+
+ public Class<? extends AttributedType> getMappedClass() {
+ return this.mappedClass;
+ }
+
+ public Set<String> getObjectClasses() {
+ return this.objectClasses;
+ }
+
+ public String getBaseDN() {
+ return this.baseDN;
+ }
+
+ public Map<String, String> getMappedProperties() {
+ return this.mappedProperties;
+ }
+
+ public Property<String> getIdProperty() {
+ return this.idProperty;
+ }
+
+ public Property<String> getBindingProperty() {
+ return this.bindingProperty;
+ }
+
+ public Class<? extends AttributedType> getRelatedAttributedType() {
+ return this.relatedAttributedType;
+ }
+
+ public String getParentMembershipAttributeName() {
+ return this.parentMembershipAttributeName;
+ }
+
+ public Map<String, String> getParentMapping() {
+ return this.parentMapping;
+ }
+
+ public Set<String> getReadOnlyAttributes() {
+ return this.readOnlyAttributes;
+ }
+
+ public int getHierarchySearchDepth() {
+ return this.hierarchySearchDepth;
+ }
+
+ private Property getBindingProperty(final String bindingPropertyName) {
+ Property bindingProperty = PropertyQueries
+ .<String>createQuery(getMappedClass())
+ .addCriteria(new NamedPropertyCriteria(bindingPropertyName)).getFirstResult();
+
+ // We don't have Java property, so actually delegate to setAttribute/getAttribute
+ if (bindingProperty == null) {
+ bindingProperty = new Property<String>() {
+
+ @Override
+ public String getName() {
+ return bindingPropertyName;
+ }
+
+ @Override
+ public Type getBaseType() {
+ return null;
+ }
+
+ @Override
+ public Class<String> getJavaClass() {
+ return String.class;
+ }
+
+ @Override
+ public AnnotatedElement getAnnotatedElement() {
+ return null;
+ }
+
+ @Override
+ public Member getMember() {
+ return null;
+ }
+
+ @Override
+ public String getValue(Object instance) {
+ if (!(instance instanceof AttributedType)) {
+ throw new IllegalStateException("Instance [ " + instance + " ] not an instance of AttributedType");
+ }
+
+ AttributedType attributedType = (AttributedType) instance;
+ Attribute<String> attr = attributedType.getAttribute(bindingPropertyName);
+ return attr!=null ? attr.getValue() : null;
+ }
+
+ @Override
+ public void setValue(Object instance, String value) {
+ if (!(instance instanceof AttributedType)) {
+ throw new IllegalStateException("Instance [ " + instance + " ] not an instance of AttributedType");
+ }
+
+ AttributedType attributedType = (AttributedType) instance;
+ attributedType.setAttribute(new Attribute(bindingPropertyName, value));
+ }
+
+ @Override
+ public Class<?> getDeclaringClass() {
+ return null;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public void setAccessible() {
+
+ }
+
+ @Override
+ public boolean isAnnotationPresent(Class annotation) {
+ return false;
+ }
+ };
+ }
+
+ return bindingProperty;
+ }
+
+ public LDAPMappingConfiguration setObjectClasses(Set<String> objectClasses) {
+ this.objectClasses = objectClasses;
+ return this;
+ }
+
+ public LDAPMappingConfiguration setBaseDN(String baseDN) {
+ this.baseDN = baseDN;
+ return this;
+ }
+
+ public LDAPMappingConfiguration addAttributeMapping(String userAttributeName, String ldapAttributeName) {
+ this.mappedProperties.put(userAttributeName, ldapAttributeName);
+ return this;
+ }
+
+ public LDAPMappingConfiguration addReadOnlyAttributeMapping(String userAttributeName, String ldapAttributeName) {
+ this.mappedProperties.put(userAttributeName, ldapAttributeName);
+ this.readOnlyAttributes.add(userAttributeName);
+ return this;
+ }
+
+ public LDAPMappingConfiguration setIdPropertyName(String idPropertyName) {
+
+ if (idPropertyName != null) {
+ this.idProperty = PropertyQueries
+ .<String>createQuery(getMappedClass())
+ .addCriteria(new NamedPropertyCriteria(idPropertyName)).getFirstResult();
+ } else {
+ this.idProperty = null;
+ }
+
+ if (IdentityType.class.isAssignableFrom(mappedClass) && idProperty == null) {
+ throw new ModelException("Id attribute not mapped to any property of [" + mappedClass + "].");
+ }
+
+ // Binding property is idProperty by default
+ if (this.bindingProperty == null) {
+ this.bindingProperty = this.idProperty;
+ }
+
+ return this;
+ }
+
+ public LDAPMappingConfiguration setRelatedAttributedType(Class<? extends AttributedType> relatedAttributedType) {
+ this.relatedAttributedType = relatedAttributedType;
+ return this;
+ }
+
+ public LDAPMappingConfiguration setParentMembershipAttributeName(String parentMembershipAttributeName) {
+ this.parentMembershipAttributeName = parentMembershipAttributeName;
+ return this;
+ }
+
+ public LDAPMappingConfiguration setParentMapping(Map<String, String> parentMapping) {
+ this.parentMapping = parentMapping;
+ return this;
+ }
+
+ public LDAPMappingConfiguration setHierarchySearchDepth(int hierarchySearchDepth) {
+ this.hierarchySearchDepth = hierarchySearchDepth;
+ return this;
+ }
+
+ public LDAPMappingConfiguration setBindingPropertyName(String bindingPropertyName) {
+ this.bindingProperty = getBindingProperty(bindingPropertyName);
+ return this;
+ }
+}
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 0000000..507d61f
--- /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;
+
+/**
+ * <p>This class provides a set of operations to manage LDAP trees.</p>
+ *
+ * @author Anil Saldhana
+ * @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
+ */
+public class LDAPOperationManager {
+
+ private static final Logger logger = Logger.getLogger(LDAPOperationManager.class);
+
+ private final LDAPIdentityStoreConfiguration config;
+ private final Map<String, Object> connectionProperties;
+
+ public LDAPOperationManager(LDAPIdentityStoreConfiguration config) throws NamingException {
+ this.config = config;
+ this.connectionProperties = Collections.unmodifiableMap(createConnectionProperties());
+ }
+
+ /**
+ * <p>
+ * Modifies the given {@link javax.naming.directory.Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE
+ * operation.
+ * </p>
+ *
+ * @param dn
+ * @param attribute
+ */
+ public void modifyAttribute(String dn, Attribute attribute) {
+ ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
+ modifyAttributes(dn, mods);
+ }
+
+ /**
+ * <p>
+ * Modifies the given {@link Attribute} instances using the given DN. This method performs a REPLACE_ATTRIBUTE
+ * operation.
+ * </p>
+ *
+ * @param dn
+ * @param attributes
+ */
+ public void modifyAttributes(String dn, NamingEnumeration<Attribute> attributes) {
+ try {
+ List<ModificationItem> modItems = new ArrayList<ModificationItem>();
+ while (attributes.hasMore()) {
+ ModificationItem modItem = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attributes.next());
+ modItems.add(modItem);
+ }
+
+ modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}));
+ } catch (NamingException ne) {
+ throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", ne);
+ }
+
+ }
+
+ /**
+ * <p>
+ * Removes the given {@link Attribute} instance using the given DN. This method performs a REMOVE_ATTRIBUTE
+ * operation.
+ * </p>
+ *
+ * @param dn
+ * @param attribute
+ */
+ public void removeAttribute(String dn, Attribute attribute) {
+ ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
+ modifyAttributes(dn, mods);
+ }
+
+ /**
+ * <p>
+ * Adds the given {@link Attribute} instance using the given DN. This method performs a ADD_ATTRIBUTE operation.
+ * </p>
+ *
+ * @param dn
+ * @param attribute
+ */
+ public void addAttribute(String dn, Attribute attribute) {
+ ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
+ modifyAttributes(dn, mods);
+ }
+
+ /**
+ * <p>
+ * Searches the LDAP tree.
+ * </p>
+ *
+ * @param baseDN
+ * @param id
+ *
+ * @return
+ */
+ public void removeEntryById(final String baseDN, final String id, final LDAPMappingConfiguration mappingConfiguration) {
+ final String filter = getFilterById(baseDN, id);
+
+ try {
+ final SearchControls cons = getSearchControls(mappingConfiguration);
+
+ execute(new LdapOperation<SearchResult>() {
+ @Override
+ public SearchResult execute(LdapContext context) throws NamingException {
+ NamingEnumeration<SearchResult> result = context.search(baseDN, filter, cons);
+
+ if (result.hasMore()) {
+ SearchResult sr = result.next();
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Removing entry [%s] with attributes: [", sr.getNameInNamespace());
+
+ NamingEnumeration<? extends Attribute> all = sr.getAttributes().getAll();
+
+ while (all.hasMore()) {
+ Attribute attribute = all.next();
+
+ logger.debugf(" %s = %s", attribute.getID(), attribute.get());
+ }
+
+ logger.debugf("]");
+ }
+ destroySubcontext(context, sr.getNameInNamespace());
+ }
+
+ result.close();
+
+ return null;
+ }
+ });
+ } catch (NamingException e) {
+ throw new ModelException("Could not remove entry from DN [" + baseDN + "] and id [" + id + "]", e);
+ }
+ }
+
+ public List<SearchResult> search(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration) throws NamingException {
+ final List<SearchResult> result = new ArrayList<SearchResult>();
+ final SearchControls cons = getSearchControls(mappingConfiguration);
+
+ try {
+ return execute(new LdapOperation<List<SearchResult>>() {
+ @Override
+ public List<SearchResult> execute(LdapContext context) throws NamingException {
+ NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
+
+ while (search.hasMoreElements()) {
+ result.add(search.nextElement());
+ }
+
+ search.close();
+
+ return result;
+ }
+ });
+ } catch (NamingException e) {
+ logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
+ throw e;
+ }
+ }
+
+ public <V extends IdentityType> List<SearchResult> searchPaginated(final String baseDN, final String filter, LDAPMappingConfiguration mappingConfiguration, final IdentityQuery<V> identityQuery) throws NamingException {
+ final List<SearchResult> result = new ArrayList<SearchResult>();
+ final SearchControls cons = getSearchControls(mappingConfiguration);
+
+ try {
+ return execute(new LdapOperation<List<SearchResult>>() {
+ @Override
+ public List<SearchResult> execute(LdapContext context) throws NamingException {
+ try {
+ byte[] cookie = (byte[])identityQuery.getPaginationContext();
+ PagedResultsControl pagedControls = new PagedResultsControl(identityQuery.getLimit(), cookie, Control.CRITICAL);
+ context.setRequestControls(new Control[] { pagedControls });
+
+ NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
+
+ while (search.hasMoreElements()) {
+ result.add(search.nextElement());
+ }
+
+ search.close();
+
+ Control[] responseControls = context.getResponseControls();
+ if (responseControls != null) {
+ for (Control respControl : responseControls) {
+ if (respControl instanceof PagedResultsResponseControl) {
+ PagedResultsResponseControl prrc = (PagedResultsResponseControl)respControl;
+ cookie = prrc.getCookie();
+ identityQuery.setPaginationContext(cookie);
+ }
+ }
+ }
+
+ return result;
+ } catch (IOException ioe) {
+ logger.errorf(ioe, "Could not query server with paginated query using DN [%s], filter [%s]", baseDN, filter);
+ throw new NamingException(ioe.getMessage());
+ }
+ }
+ });
+ } catch (NamingException e) {
+ logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
+ throw e;
+ }
+ }
+
+ private SearchControls getSearchControls(LDAPMappingConfiguration mappingConfiguration) {
+ final SearchControls cons = new SearchControls();
+
+ cons.setSearchScope(SUBTREE_SCOPE);
+ cons.setReturningObjFlag(false);
+
+ List<String> returningAttributes = getReturningAttributes(mappingConfiguration);
+
+ cons.setReturningAttributes(returningAttributes.toArray(new String[returningAttributes.size()]));
+ return cons;
+ }
+
+ public String getFilterById(String baseDN, String id) {
+ String filter = null;
+
+ if (this.config.isActiveDirectory()) {
+ final String strObjectGUID = "<GUID=" + id + ">";
+
+ try {
+ Attributes attributes = execute(new LdapOperation<Attributes>() {
+ @Override
+ public Attributes execute(LdapContext context) throws NamingException {
+ return context.getAttributes(strObjectGUID);
+ }
+ });
+
+ byte[] objectGUID = (byte[]) attributes.get(LDAPConstants.OBJECT_GUID).get();
+
+ filter = "(&(objectClass=*)(" + getUniqueIdentifierAttributeName() + LDAPConstants.EQUAL + LDAPUtil.convertObjectGUIToByteString(objectGUID) + "))";
+ } catch (NamingException ne) {
+ return filter;
+ }
+ }
+
+ if (filter == null) {
+ filter = "(&(objectClass=*)(" + getUniqueIdentifierAttributeName() + LDAPConstants.EQUAL + id + "))";
+ }
+
+ return filter;
+ }
+
+ public SearchResult lookupById(final String baseDN, final String id, final LDAPMappingConfiguration mappingConfiguration) {
+ final String filter = getFilterById(baseDN, id);
+
+ try {
+ final SearchControls cons = getSearchControls(mappingConfiguration);
+
+ return execute(new LdapOperation<SearchResult>() {
+ @Override
+ public SearchResult execute(LdapContext context) throws NamingException {
+ NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
+
+ try {
+ if (search.hasMoreElements()) {
+ return search.next();
+ }
+ } finally {
+ if (search != null) {
+ search.close();
+ }
+ }
+
+ return null;
+ }
+ });
+ } catch (NamingException e) {
+ throw new ModelException("Could not query server using DN [" + baseDN + "] and filter [" + filter + "]", e);
+ }
+ }
+
+ /**
+ * <p>
+ * Destroys a subcontext with the given DN from the LDAP tree.
+ * </p>
+ *
+ * @param dn
+ */
+ private void destroySubcontext(LdapContext context, final String dn) {
+ try {
+ NamingEnumeration<Binding> enumeration = null;
+
+ try {
+ enumeration = context.listBindings(dn);
+
+ while (enumeration.hasMore()) {
+ Binding binding = enumeration.next();
+ String name = binding.getNameInNamespace();
+
+ destroySubcontext(context, name);
+ }
+
+ context.unbind(dn);
+ } finally {
+ try {
+ enumeration.close();
+ } catch (Exception e) {
+ }
+ }
+ } catch (Exception e) {
+ throw new ModelException("Could not unbind DN [" + dn + "]", e);
+ }
+ }
+
+ /**
+ * <p>
+ * Performs a simple authentication using the given DN and password to bind to the authentication context.
+ * </p>
+ *
+ * @param dn
+ * @param password
+ *
+ * @return
+ */
+ public boolean authenticate(String dn, String password) {
+ InitialContext authCtx = null;
+
+ try {
+ Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
+
+ env.put(Context.SECURITY_PRINCIPAL, dn);
+ env.put(Context.SECURITY_CREDENTIALS, password);
+
+ // Never use connection pool to prevent password caching
+ env.put("com.sun.jndi.ldap.connect.pool", "false");
+
+ authCtx = new InitialLdapContext(env, null);
+
+ return true;
+ } catch (Exception e) {
+ if (logger.isDebugEnabled()) {
+ logger.debugf(e, "Authentication failed for DN [%s]", dn);
+ }
+
+ return false;
+ } finally {
+ if (authCtx != null) {
+ try {
+ authCtx.close();
+ } catch (NamingException e) {
+
+ }
+ }
+ }
+ }
+
+ public void modifyAttributes(final String dn, final ModificationItem[] mods) {
+ try {
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Modifying attributes for entry [%s]: [", dn);
+
+ for (ModificationItem item : mods) {
+ Object values;
+
+ if (item.getAttribute().size() > 0) {
+ values = item.getAttribute().get();
+ } else {
+ values = "No values";
+ }
+
+ logger.debugf(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
+ }
+
+ logger.debugf("]");
+ }
+
+ execute(new LdapOperation<Void>() {
+ @Override
+ public Void execute(LdapContext context) throws NamingException {
+ context.modifyAttributes(dn, mods);
+ return null;
+ }
+ });
+ } catch (NamingException e) {
+ throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
+ }
+ }
+
+ public void createSubContext(final String name, final Attributes attributes) {
+ try {
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Creating entry [%s] with attributes: [", name);
+
+ NamingEnumeration<? extends Attribute> all = attributes.getAll();
+
+ while (all.hasMore()) {
+ Attribute attribute = all.next();
+
+ logger.debugf(" %s = %s", attribute.getID(), attribute.get());
+ }
+
+ logger.debugf("]");
+ }
+
+ execute(new LdapOperation<Void>() {
+ @Override
+ public Void execute(LdapContext context) throws NamingException {
+ DirContext subcontext = context.createSubcontext(name, attributes);
+
+ subcontext.close();
+
+ return null;
+ }
+ });
+ } catch (NamingException e) {
+ throw new ModelException("Error creating subcontext [" + name + "]", e);
+ }
+ }
+
+ private String getUniqueIdentifierAttributeName() {
+ return this.config.getUniqueIdentifierAttributeName();
+ }
+
+ private NamingEnumeration<SearchResult> createEmptyEnumeration() {
+ return new NamingEnumeration<SearchResult>() {
+ @Override
+ public SearchResult next() throws NamingException {
+ return null; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public boolean hasMore() throws NamingException {
+ return false; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void close() throws NamingException {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public boolean hasMoreElements() {
+ return false; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public SearchResult nextElement() {
+ return null; //To change body of implemented methods use File | Settings | File Templates.
+ }
+ };
+ }
+
+ public Attributes getAttributes(final String entryUUID, final String baseDN, LDAPMappingConfiguration mappingConfiguration) {
+ SearchResult search = lookupById(baseDN, entryUUID, mappingConfiguration);
+
+ if (search == null) {
+ throw new ModelException("Couldn't find item with entryUUID [" + entryUUID + "] and baseDN [" + baseDN + "]");
+ }
+
+ return search.getAttributes();
+ }
+
+ public String decodeEntryUUID(final Object entryUUID) {
+ String id;
+
+ if (this.config.isActiveDirectory()) {
+ id = LDAPUtil.decodeObjectGUID((byte[]) entryUUID);
+ } else {
+ id = entryUUID.toString();
+ }
+
+ return id;
+ }
+
+ private LdapContext createLdapContext() throws NamingException {
+ return new InitialLdapContext(new Hashtable<Object, Object>(this.connectionProperties), null);
+ }
+
+ private Map<String, Object> createConnectionProperties() {
+ HashMap<String, Object> env = new HashMap<String, Object>();
+
+ env.put(Context.INITIAL_CONTEXT_FACTORY, this.config.getFactoryName());
+ env.put(Context.SECURITY_AUTHENTICATION, this.config.getAuthType());
+
+ String protocol = this.config.getProtocol();
+
+ if (protocol != null) {
+ env.put(Context.SECURITY_PROTOCOL, protocol);
+ }
+
+ String bindDN = this.config.getBindDN();
+
+ char[] bindCredential = null;
+
+ if (this.config.getBindCredential() != null) {
+ bindCredential = this.config.getBindCredential().toCharArray();
+ }
+
+ if (bindDN != null) {
+ env.put(Context.SECURITY_PRINCIPAL, bindDN);
+ env.put(Context.SECURITY_CREDENTIALS, bindCredential);
+ }
+
+ String url = this.config.getLdapURL();
+
+ if (url == null) {
+ throw new RuntimeException("url");
+ }
+
+ env.put(Context.PROVIDER_URL, url);
+
+ // Just dump the additional properties
+ Properties additionalProperties = this.config.getConnectionProperties();
+
+ if (additionalProperties != null) {
+ for (Object key : additionalProperties.keySet()) {
+ env.put(key.toString(), additionalProperties.getProperty(key.toString()));
+ }
+ }
+
+ if (config.isActiveDirectory()) {
+ env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debugf("Creating LdapContext using properties: [%s]", env);
+ }
+
+ return env;
+ }
+
+ private <R> R execute(LdapOperation<R> operation) throws NamingException {
+ LdapContext context = null;
+
+ try {
+ context = createLdapContext();
+ return operation.execute(context);
+ } catch (NamingException ne) {
+ logger.error("Could not create Ldap context or operation execution error.", ne);
+ throw ne;
+ } finally {
+ if (context != null) {
+ try {
+ context.close();
+ } catch (NamingException ne) {
+ logger.error("Could not close Ldap context.", ne);
+ }
+ }
+ }
+ }
+
+ private interface LdapOperation<R> {
+ R execute(LdapContext context) throws NamingException;
+ }
+
+ private List<String> getReturningAttributes(final LDAPMappingConfiguration mappingConfiguration) {
+ List<String> returningAttributes = new ArrayList<String>();
+
+ if (mappingConfiguration != null) {
+ returningAttributes.addAll(mappingConfiguration.getMappedProperties().values());
+
+ returningAttributes.add(mappingConfiguration.getParentMembershipAttributeName());
+
+// for (LDAPMappingConfiguration relationshipConfig : this.config.getRelationshipConfigs()) {
+// if (relationshipConfig.getRelatedAttributedType().equals(mappingConfiguration.getMappedClass())) {
+// returningAttributes.addAll(relationshipConfig.getMappedProperties().values());
+// }
+// }
+ } else {
+ returningAttributes.add("*");
+ }
+
+ returningAttributes.add(getUniqueIdentifierAttributeName());
+ returningAttributes.add(LDAPConstants.CREATE_TIMESTAMP);
+ returningAttributes.add(LDAPConstants.OBJECT_CLASS);
+
+ return returningAttributes;
+ }
+}
\ 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 0000000..f08ff85
--- /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;
+
+/**
+ * <p>Utility class for working with LDAP.</p>
+ *
+ * @author Pedro Igor
+ */
+public class LDAPUtil {
+
+ /**
+ * <p>Formats the given date.</p>
+ *
+ * @param date The Date to format.
+ *
+ * @return A String representing the formatted date.
+ */
+ public static final String formatDate(Date date) {
+ if (date == null) {
+ throw new IllegalArgumentException("You must provide a date.");
+ }
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'.0Z'");
+
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ return dateFormat.format(date);
+ }
+
+ /**
+ * <p>
+ * Parses dates/time stamps stored in LDAP. Some possible values:
+ * </p>
+ * <ul>
+ * <li>20020228150820</li>
+ * <li>20030228150820Z</li>
+ * <li>20050228150820.12</li>
+ * <li>20060711011740.0Z</li>
+ * </ul>
+ *
+ * @param date The date string to parse from.
+ *
+ * @return the Date.
+ */
+ public static final Date parseDate(String date) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+
+ try {
+ if (date.endsWith("Z")) {
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ } else {
+ dateFormat.setTimeZone(TimeZone.getDefault());
+ }
+
+ return dateFormat.parse(date);
+ } catch (Exception e) {
+ throw new ModelException("Error converting ldap date.", e);
+ }
+ }
+
+
+
+ /**
+ * <p>Creates a byte-based {@link String} representation of a raw byte array representing the value of the
+ * <code>objectGUID</code> attribute retrieved from Active Directory.</p>
+ *
+ * <p>The returned string is useful to perform queries on AD based on the <code>objectGUID</code> value. Eg.:</p>
+ *
+ * <p>
+ * String filter = "(&(objectClass=*)(objectGUID" + EQUAL + convertObjectGUIToByteString(objectGUID) + "))";
+ * </p>
+ *
+ * @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
+ * Active Directory.
+ *
+ * @return A byte-based String representation in the form of \[0]\[1]\[2]\[3]\[4]\[5]\[6]\[7]\[8]\[9]\[10]\[11]\[12]\[13]\[14]\[15]
+ */
+ public static String convertObjectGUIToByteString(byte[] objectGUID) {
+ StringBuilder result = new StringBuilder();
+
+ for (int i = 0; i < objectGUID.length; i++) {
+ String transformed = prefixZeros((int) objectGUID[i] & 0xFF);
+ result.append("\\");
+ result.append(transformed);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * <p>Decode a raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from Active
+ * Directory.</p>
+ *
+ * <p>The returned string is useful to directly bind an entry. Eg.:</p>
+ *
+ * <p>
+ * String bindingString = decodeObjectGUID(objectGUID);
+ * <br/>
+ * Attributes attributes = ctx.getAttributes(bindingString);
+ * </p>
+ *
+ * @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
+ * Active Directory.
+ *
+ * @return A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15].
+ */
+ public static String decodeObjectGUID(byte[] objectGUID) {
+ StringBuilder displayStr = new StringBuilder();
+
+ displayStr.append(convertToDashedString(objectGUID));
+
+ return displayStr.toString();
+ }
+
+ private static String convertToDashedString(byte[] objectGUID) {
+ StringBuilder displayStr = new StringBuilder();
+
+ displayStr.append(prefixZeros((int) objectGUID[3] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[2] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[1] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[0] & 0xFF));
+ displayStr.append("-");
+ displayStr.append(prefixZeros((int) objectGUID[5] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[4] & 0xFF));
+ displayStr.append("-");
+ displayStr.append(prefixZeros((int) objectGUID[7] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[6] & 0xFF));
+ displayStr.append("-");
+ displayStr.append(prefixZeros((int) objectGUID[8] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[9] & 0xFF));
+ displayStr.append("-");
+ displayStr.append(prefixZeros((int) objectGUID[10] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[11] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[12] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[13] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[14] & 0xFF));
+ displayStr.append(prefixZeros((int) objectGUID[15] & 0xFF));
+
+ return displayStr.toString();
+ }
+
+ private static String prefixZeros(int value) {
+ if (value <= 0xF) {
+ StringBuilder sb = new StringBuilder("0");
+ sb.append(Integer.toHexString(value));
+ return sb.toString();
+ } else {
+ return Integer.toHexString(value);
+ }
+ }
+
+
+}
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 f48880c..370e0f0 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<String> supportedCredentialTypes = new HashSet<String>();
- public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, PartitionManager partitionManager) {
+ public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, LDAPIdentityStore ldapIdentityStore) {
this.factory = factory;
this.session = session;
this.model = model;
- this.partitionManager = partitionManager;
+ this.ldapIdentityStore = ldapIdentityStore;
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
String editModeString = model.getConfig().get(LDAPConstants.EDIT_MODE);
if (editModeString == null) {
@@ -69,16 +65,6 @@ public class LDAPFederationProvider implements UserFederationProvider {
}
}
- private ModelException convertIDMException(IdentityManagementException ie) {
- Throwable realCause = ie;
- while (realCause.getCause() != null) {
- realCause = realCause.getCause();
- }
-
- // Use the message from the realCause
- return new ModelException(realCause.getMessage(), ie);
- }
-
public KeycloakSession getSession() {
return session;
}
@@ -87,8 +73,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
return model;
}
- public PartitionManager getPartitionManager() {
- return partitionManager;
+ public LDAPIdentityStore getLdapIdentityStore() {
+ return this.ldapIdentityStore;
}
@Override
@@ -125,22 +111,18 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public boolean synchronizeRegistrations() {
- return "true".equalsIgnoreCase(model.getConfig().get(SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE;
+ return "true".equalsIgnoreCase(model.getConfig().get(LDAPConstants.SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE;
}
@Override
public UserModel register(RealmModel realm, UserModel user) {
- if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");;
+ if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");
if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
- try {
- User picketlinkUser = LDAPUtils.addUser(this.partitionManager, user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmail());
- user.setAttribute(LDAP_ID, picketlinkUser.getId());
- 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<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults) {
List<UserModel> searchResults =new LinkedList<UserModel>();
- try {
- Map<String, User> plUsers = searchPicketlink(attributes, maxResults);
- for (User user : plUsers.values()) {
- if (session.userStorage().getUserByUsername(user.getLoginName(), realm) == null) {
- UserModel imported = importUserFromPicketlink(realm, user);
- searchResults.add(imported);
- }
+
+ Map<String, LDAPUser> ldapUsers = searchLDAP(attributes, maxResults);
+ for (LDAPUser ldapUser : ldapUsers.values()) {
+ if (session.userStorage().getUserByUsername(ldapUser.getLoginName(), realm) == null) {
+ UserModel imported = importUserFromLDAP(realm, ldapUser);
+ searchResults.add(imported);
}
- } catch (IdentityManagementException ie) {
- throw convertIDMException(ie);
}
+
return searchResults;
}
- protected Map<String, User> searchPicketlink(Map<String, String> attributes, int maxResults) {
- IdentityManager identityManager = getIdentityManager();
- Map<String, User> results = new HashMap<String, User>();
+ protected Map<String, LDAPUser> searchLDAP(Map<String, String> attributes, int maxResults) {
+
+ Map<String, LDAPUser> results = new HashMap<String, LDAPUser>();
if (attributes.containsKey(USERNAME)) {
- User user = BasicModel.getUser(identityManager, attributes.get(USERNAME));
+ LDAPUser user = LDAPUtils.getUser(this.ldapIdentityStore, attributes.get(USERNAME));
if (user != null) {
results.put(user.getLoginName(), user);
}
}
if (attributes.containsKey(EMAIL)) {
- User user = queryByEmail(identityManager, attributes.get(EMAIL));
+ LDAPUser user = queryByEmail(attributes.get(EMAIL));
if (user != null) {
results.put(user.getLoginName(), user);
}
}
if (attributes.containsKey(FIRST_NAME) || attributes.containsKey(LAST_NAME)) {
- IdentityQuery<User> query = identityManager.createIdentityQuery(User.class);
+ IdentityQueryBuilder queryBuilder = this.ldapIdentityStore.createQueryBuilder();
+ IdentityQuery<LDAPUser> query = queryBuilder.createIdentityQuery(LDAPUser.class);
if (attributes.containsKey(FIRST_NAME)) {
- query.setParameter(User.FIRST_NAME, attributes.get(FIRST_NAME));
+ query.where(queryBuilder.equal(LDAPUser.FIRST_NAME, attributes.get(FIRST_NAME)));
}
if (attributes.containsKey(LAST_NAME)) {
- query.setParameter(User.LAST_NAME, attributes.get(LAST_NAME));
+ query.where(queryBuilder.equal(LDAPUser.LAST_NAME, attributes.get(LAST_NAME)));
}
query.setLimit(maxResults);
- List<User> agents = query.getResultList();
- for (User user : agents) {
+ List<LDAPUser> users = query.getResultList();
+ for (LDAPUser user : users) {
results.put(user.getLoginName(), user);
}
}
@@ -211,85 +188,69 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public boolean isValid(UserModel local) {
- try {
- User picketlinkUser = LDAPUtils.getUser(partitionManager, local.getUsername());
- if (picketlinkUser == null) {
- 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;
- }
+ LDAPUser ldapUser = LDAPUtils.getUser(this.ldapIdentityStore, username);
+ if (ldapUser == null) {
+ return null;
+ }
- return importUserFromPicketlink(realm, picketlinkUser);
- } catch (IdentityManagementException ie) {
- throw convertIDMException(ie);
+ // 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;
}
- }
- public IdentityManager getIdentityManager() {
- return partitionManager.createIdentityManager();
+ return importUserFromLDAP(realm, ldapUser);
}
- protected UserModel importUserFromPicketlink(RealmModel realm, User picketlinkUser) {
- String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
+ protected UserModel importUserFromLDAP(RealmModel realm, LDAPUser ldapUser) {
+ String email = (ldapUser.getEmail() != null && ldapUser.getEmail().trim().length() > 0) ? ldapUser.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());
+ 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, picketlinkUser.getLoginName());
+ UserModel imported = session.userStorage().addUser(realm, ldapUser.getLoginName());
imported.setEnabled(true);
imported.setEmail(email);
- imported.setFirstName(picketlinkUser.getFirstName());
- imported.setLastName(picketlinkUser.getLastName());
+ imported.setFirstName(ldapUser.getFirstName());
+ imported.setLastName(ldapUser.getLastName());
imported.setFederationLink(model.getId());
- imported.setAttribute(LDAP_ID, picketlinkUser.getId());
+ imported.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getId());
+ imported.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getEntryDN());
- logger.debugf("Added new user from LDAP. Username: " + imported.getUsername() + ", Email: ", imported.getEmail() + ", LDAP_ID: " + picketlinkUser.getId());
+ logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
+ ldapUser.getId(), ldapUser.getEntryDN());
return proxy(imported);
}
- protected User queryByEmail(IdentityManager identityManager, String email) throws IdentityManagementException {
- return LDAPUtils.getUserByEmail(identityManager, email);
+ protected LDAPUser queryByEmail(String email) {
+ return LDAPUtils.getUserByEmail(this.ldapIdentityStore, email);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
- IdentityManager identityManager = getIdentityManager();
-
- try {
- User picketlinkUser = queryByEmail(identityManager, email);
- if (picketlinkUser == null) {
- 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;
- }
+ LDAPUser ldapUser = queryByEmail(email);
+ if (ldapUser == null) {
+ return null;
+ }
- return importUserFromPicketlink(realm, picketlinkUser);
- } catch (IdentityManagementException ie) {
- throw convertIDMException(ie);
+ // 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<UserCredentialModel> input) {
for (UserCredentialModel cred : input) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
- return validPassword(user.getUsername(), cred.getValue());
+ return validPassword(user, cred.getValue());
} else {
return false; // invalid cred type
}
@@ -353,7 +310,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
UserModel user = findOrCreateAuthenticatedUser(realm, username);
if (user == null) {
- logger.warn("Kerberos/SPNEGO authentication succeeded with username [" + username + "], but couldn't find or create user with federation provider [" + model.getDisplayName() + "]");
+ logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getDisplayName());
return CredentialValidationOutput.failed();
} else {
String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
@@ -375,24 +332,23 @@ public class LDAPFederationProvider implements UserFederationProvider {
@Override
public void close() {
- //To change body of implemented methods use File | Settings | File Templates.
}
- protected void importPicketlinkUsers(RealmModel realm, List<User> users, UserFederationProviderModel fedModel) {
- for (User picketlinkUser : users) {
- String username = picketlinkUser.getLoginName();
+ protected void importLDAPUsers(RealmModel realm, List<LDAPUser> ldapUsers, UserFederationProviderModel fedModel) {
+ for (LDAPUser ldapUser : ldapUsers) {
+ String username = ldapUser.getLoginName();
UserModel currentUser = session.userStorage().getUserByUsername(username, realm);
if (currentUser == null) {
// Add new user to Keycloak
- importUserFromPicketlink(realm, picketlinkUser);
+ importUserFromLDAP(realm, ldapUser);
} else {
- if ((fedModel.getId().equals(currentUser.getFederationLink())) && (picketlinkUser.getId().equals(currentUser.getAttribute(LDAPFederationProvider.LDAP_ID)))) {
+ if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getId().equals(currentUser.getAttribute(LDAPConstants.LDAP_ID)))) {
// Update keycloak user
- String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
+ String email = (ldapUser.getEmail() != null && ldapUser.getEmail().trim().length() > 0) ? ldapUser.getEmail() : null;
currentUser.setEmail(email);
- currentUser.setFirstName(picketlinkUser.getFirstName());
- currentUser.setLastName(picketlinkUser.getLastName());
+ currentUser.setFirstName(ldapUser.getFirstName());
+ currentUser.setLastName(ldapUser.getLastName());
logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
} else {
logger.warnf("User '%s' is not updated during sync as he is not linked to federation provider '%s'", username, fedModel.getDisplayName());
@@ -404,29 +360,29 @@ public class LDAPFederationProvider implements UserFederationProvider {
/**
* Called after successful kerberos authentication
*
- * @param realm
+ * @param realm realm
* @param username username without realm prefix
- * @return
+ * @return finded or newly created user
*/
protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) {
UserModel user = session.userStorage().getUserByUsername(username, realm);
if (user != null) {
- logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage");
+ logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", username);
if (!model.getId().equals(user.getFederationLink())) {
- logger.warn("User with username " + username + " already exists, but is not linked to provider [" + model.getDisplayName() + "]");
+ logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]", username, model.getDisplayName());
return null;
} else if (isValid(user)) {
return proxy(user);
} else {
- logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getDisplayName() +
- "] but is not valid. Stale LDAP_ID on local user is: " + user.getAttribute(LDAP_ID));
+ logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
+ username, model.getDisplayName(), user.getAttribute(LDAPConstants.LDAP_ID));
logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user);
}
}
// Creating user to local storage
- logger.debug("Kerberos authenticated user " + username + " not in Keycloak storage. Creating him");
+ logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", username);
return getUserByUsername(realm, username);
}
}
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 c197052..a498a93 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<User> userQuery = partitionMgr.createIdentityManager().createIdentityQuery(User.class);
+ LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
+ IdentityQuery<LDAPUser> userQuery = ldapIdentityStore.createQueryBuilder().createIdentityQuery(LDAPUser.class);
syncImpl(sessionFactory, userQuery, realmId, model);
// TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
@@ -91,26 +87,23 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
@Override
public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
- logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date() + ", last sync time: " + lastSync, realmId, model.getDisplayName());
+ logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: %s, last sync time: " + lastSync, realmId, model.getDisplayName(), new Date().toString());
- PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
- PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
+ LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
// Sync newly created users
- IdentityManager identityManager = partitionMgr.createIdentityManager();
- IdentityQueryBuilder queryBuilder = identityManager.getQueryBuilder();
+ IdentityQueryBuilder queryBuilder = ldapIdentityStore.createQueryBuilder();
Condition condition = queryBuilder.greaterThanOrEqualTo(IdentityType.CREATED_DATE, lastSync);
- IdentityQuery<User> userQuery = queryBuilder.createIdentityQuery(User.class).where(condition);
+ IdentityQuery<LDAPUser> userQuery = queryBuilder.createIdentityQuery(LDAPUser.class).where(condition);
syncImpl(sessionFactory, userQuery, realmId, model);
// Sync updated users
- queryBuilder = identityManager.getQueryBuilder();
condition = queryBuilder.greaterThanOrEqualTo(LDAPUtils.MODIFY_DATE, lastSync);
- userQuery = queryBuilder.createIdentityQuery(User.class).where(condition);
+ userQuery = queryBuilder.createIdentityQuery(LDAPUser.class).where(condition);
syncImpl(sessionFactory, userQuery, realmId, model);
}
- protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<User> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
+ protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<LDAPUser> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
boolean pagination = Boolean.parseBoolean(fedModel.getConfig().get(LDAPConstants.PAGINATION));
if (pagination) {
@@ -119,36 +112,36 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
boolean nextPage = true;
while (nextPage) {
userQuery.setLimit(pageSize);
- final List<User> users = userQuery.getResultList();
+ final List<LDAPUser> users = userQuery.getResultList();
nextPage = userQuery.getPaginationContext() != null;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
- importPicketlinkUsers(session, realmId, fedModel, users);
+ importLdapUsers(session, realmId, fedModel, users);
}
});
}
} else {
// LDAP pagination not available. Do everything in single transaction
- final List<User> users = userQuery.getResultList();
+ final List<LDAPUser> users = userQuery.getResultList();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
- importPicketlinkUsers(session, realmId, fedModel, users);
+ importLdapUsers(session, realmId, fedModel, users);
}
});
}
}
- protected void importPicketlinkUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<User> users) {
+ protected void importLdapUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<LDAPUser> ldapUsers) {
RealmModel realm = session.realms().getRealm(realmId);
LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
- ldapFedProvider.importPicketlinkUsers(realm, users, fedModel);
+ ldapFedProvider.importLDAPUsers(realm, ldapUsers, fedModel);
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {
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 0000000..22aa55a
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPIdentityStoreRegistry {
+
+ private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
+
+ private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
+
+ public LDAPIdentityStore getLdapStore(UserFederationProviderModel model) {
+ LDAPIdentityStoreContext context = ldapStores.get(model.getId());
+
+ // Ldap config might have changed for the realm. In this case, we must re-initialize
+ Map<String, String> config = model.getConfig();
+ if (context == null || !config.equals(context.config)) {
+ logLDAPConfig(model.getId(), config);
+
+ LDAPIdentityStore store = createLdapIdentityStore(config);
+ context = new LDAPIdentityStoreContext(config, store);
+ ldapStores.put(model.getId(), context);
+ }
+ return context.store;
+ }
+
+ // Don't log LDAP password
+ private void logLDAPConfig(String fedProviderId, Map<String, String> ldapConfig) {
+ Map<String, String> copy = new HashMap<String, String>(ldapConfig);
+ copy.remove(LDAPConstants.BIND_CREDENTIAL);
+ logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderId + ", LDAP Configuration: " + copy);
+ }
+
+ /**
+ * @param ldapConfig from realm
+ * @return PartitionManager instance based on LDAP store
+ */
+ public static LDAPIdentityStore createLdapIdentityStore(Map<String,String> ldapConfig) {
+ Properties connectionProps = new Properties();
+ if (ldapConfig.containsKey(LDAPConstants.CONNECTION_POOLING)) {
+ connectionProps.put("com.sun.jndi.ldap.connect.pool", ldapConfig.get(LDAPConstants.CONNECTION_POOLING));
+ }
+
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.prefsize", "5");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.timeout", "300000");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain");
+ checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
+
+ String vendor = ldapConfig.get(LDAPConstants.VENDOR);
+
+ boolean activeDirectory = vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
+
+ String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
+ if (ldapLoginNameMapping == null) {
+ ldapLoginNameMapping = activeDirectory ? LDAPConstants.CN : LDAPConstants.UID;
+ }
+
+ String ldapFirstNameMapping = activeDirectory ? "givenName" : LDAPConstants.CN;
+ String createTimestampMapping = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
+ String modifyTimestampMapping = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
+ String[] userObjectClasses = getUserObjectClasses(ldapConfig);
+
+ boolean pagination = ldapConfig.containsKey(LDAPConstants.PAGINATION) ? Boolean.parseBoolean(ldapConfig.get(LDAPConstants.PAGINATION)) : false;
+ boolean userAccountControlsAfterPasswordUpdate = ldapConfig.containsKey(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE) ?
+ Boolean.parseBoolean(ldapConfig.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE)) : false;
+
+ // Differences of unique attribute among various vendors
+ String uniqueIdentifierAttributeName = LDAPConstants.ENTRY_UUID;
+ if (vendor != null) {
+ switch (vendor) {
+ case LDAPConstants.VENDOR_RHDS:
+ uniqueIdentifierAttributeName = "nsuniqueid";
+ break;
+ case LDAPConstants.VENDOR_TIVOLI:
+ uniqueIdentifierAttributeName = "uniqueidentifier";
+ break;
+ case LDAPConstants.VENDOR_ACTIVE_DIRECTORY:
+ uniqueIdentifierAttributeName = LDAPConstants.OBJECT_GUID;
+ }
+ }
+
+ LDAPIdentityStoreConfiguration ldapStoreConfig = new LDAPIdentityStoreConfiguration()
+ .setConnectionProperties(connectionProps)
+ .setBaseDN(ldapConfig.get(LDAPConstants.BASE_DN))
+ .setBindDN(ldapConfig.get(LDAPConstants.BIND_DN))
+ .setBindCredential(ldapConfig.get(LDAPConstants.BIND_CREDENTIAL))
+ .setLdapURL(ldapConfig.get(LDAPConstants.CONNECTION_URL))
+ .setActiveDirectory(activeDirectory)
+ .setPagination(pagination)
+ .setUniqueIdentifierAttributeName(uniqueIdentifierAttributeName)
+ .setFactoryName("com.sun.jndi.ldap.LdapCtxFactory")
+ .setAuthType("simple")
+ .setUserAccountControlsAfterPasswordUpdate(userAccountControlsAfterPasswordUpdate);
+
+ LDAPMappingConfiguration ldapUserMappingConfig = ldapStoreConfig
+ .mappingConfig(LDAPUser.class)
+ .setBaseDN(ldapConfig.get(LDAPConstants.USER_DN_SUFFIX))
+ .setObjectClasses(new HashSet<String>(Arrays.asList(userObjectClasses)))
+ .setIdPropertyName("loginName")
+ .addAttributeMapping("loginName", ldapLoginNameMapping)
+ .addAttributeMapping("firstName", ldapFirstNameMapping)
+ .addAttributeMapping("lastName", LDAPConstants.SN)
+ .addAttributeMapping("email", LDAPConstants.EMAIL)
+ .addReadOnlyAttributeMapping("createdDate", createTimestampMapping)
+ .addReadOnlyAttributeMapping("modifyDate", modifyTimestampMapping);
+
+ if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) {
+ ldapUserMappingConfig.setBindingPropertyName("fullName");
+ ldapUserMappingConfig.addAttributeMapping("fullName", LDAPConstants.CN);
+ logger.infof("Using 'cn' attribute for DN of user and 'sAMAccountName' for username");
+ }
+
+ return new LDAPIdentityStore(ldapStoreConfig);
+ }
+
+ private static void checkSystemProperty(String name, String defaultValue) {
+ if (System.getProperty(name) == null) {
+ System.setProperty(name, defaultValue);
+ }
+ }
+
+ // Parse array of strings like [ "inetOrgPerson", "organizationalPerson" ] from the string like: "inetOrgPerson, organizationalPerson"
+ private static String[] getUserObjectClasses(Map<String,String> ldapConfig) {
+ String objClassesCfg = ldapConfig.get(LDAPConstants.USER_OBJECT_CLASSES);
+ String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson, organizationalPerson";
+
+ String[] objectClasses = objClassesStr.split(",");
+
+ // Trim them
+ String[] userObjectClasses = new String[objectClasses.length];
+ for (int i=0 ; i<objectClasses.length ; i++) {
+ userObjectClasses[i] = objectClasses[i].trim();
+ }
+ return userObjectClasses;
+ }
+
+ private class LDAPIdentityStoreContext {
+
+ private LDAPIdentityStoreContext(Map<String,String> config, LDAPIdentityStore store) {
+ this.config = config;
+ this.store = store;
+ }
+
+ private Map<String,String> config;
+ private LDAPIdentityStore store;
+ }
+}
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 db0e9b8..9753592 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -24,99 +23,102 @@ public class LDAPUtils {
public static QueryParameter MODIFY_DATE = new AttributeParameter("modifyDate");
- public static User addUser(PartitionManager partitionManager, String username, String firstName, String lastName, String email) {
- IdentityManager identityManager = getIdentityManager(partitionManager);
-
- if (BasicModel.getUser(identityManager, username) != null) {
+ public static LDAPUser addUser(LDAPIdentityStore ldapIdentityStore, String username, String firstName, String lastName, String email) {
+ if (getUser(ldapIdentityStore, username) != null) {
throw new ModelDuplicateException("User with same username already exists");
}
- if (getUserByEmail(identityManager, email) != null) {
+ if (getUserByEmail(ldapIdentityStore, email) != null) {
throw new ModelDuplicateException("User with same email already exists");
}
- User picketlinkUser = new User(username);
- picketlinkUser.setFirstName(firstName);
- picketlinkUser.setLastName(lastName);
- picketlinkUser.setEmail(email);
- picketlinkUser.setAttribute(new Attribute("fullName", getFullName(username, firstName, lastName)));
- identityManager.add(picketlinkUser);
- return picketlinkUser;
+ LDAPUser ldapUser = new LDAPUser(username);
+ ldapUser.setFirstName(firstName);
+ ldapUser.setLastName(lastName);
+ ldapUser.setEmail(email);
+ ldapUser.setAttribute(new Attribute<String>("fullName", getFullName(username, firstName, lastName)));
+ ldapIdentityStore.add(ldapUser);
+ return ldapUser;
}
- public static User updateUser(PartitionManager partitionManager, String username, String firstName, String lastName, String email) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- User picketlinkUser = BasicModel.getUser(idmManager, username);
- picketlinkUser.setFirstName(firstName);
- picketlinkUser.setLastName(lastName);
- picketlinkUser.setEmail(email);
- idmManager.update(picketlinkUser);
- return picketlinkUser;
+ public static LDAPUser updateUser(LDAPIdentityStore ldapIdentityStore, String username, String firstName, String lastName, String email) {
+ LDAPUser ldapUser = getUser(ldapIdentityStore, username);
+ ldapUser.setFirstName(firstName);
+ ldapUser.setLastName(lastName);
+ ldapUser.setEmail(email);
+ ldapIdentityStore.update(ldapUser);
+ return ldapUser;
}
- public static void updatePassword(PartitionManager partitionManager, User picketlinkUser, String password) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- idmManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
+ public static void updatePassword(LDAPIdentityStore ldapIdentityStore, UserModel user, String password) {
+ LDAPUser ldapUser = convertUserForPasswordUpdate(user);
+
+ ldapIdentityStore.updatePassword(ldapUser, password);
}
- public static boolean validatePassword(PartitionManager partitionManager, String username, String password) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
+ 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 User getUser(PartitionManager partitionManager, String username) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- return BasicModel.getUser(idmManager, username);
+ public static boolean validatePassword(LDAPIdentityStore ldapIdentityStore, LDAPUser user, String password) {
+ return ldapIdentityStore.validatePassword(user, password);
}
+ public static LDAPUser getUser(LDAPIdentityStore ldapIdentityStore, String username) {
+ return ldapIdentityStore.getUser(username);
+ }
+
+ // Put just username and entryDN as these are needed by LDAPIdentityStore for passwordUpdate
+ private static LDAPUser convertUserForPasswordUpdate(UserModel kcUser) {
+ LDAPUser ldapUser = new LDAPUser(kcUser.getUsername());
+ String ldapEntryDN = kcUser.getAttribute(LDAPConstants.LDAP_ENTRY_DN);
+ if (ldapEntryDN != null) {
+ ldapUser.setEntryDN(ldapEntryDN);
+ }
+ return ldapUser;
+ }
- public static User getUserByEmail(IdentityManager idmManager, String email) throws IdentityManagementException {
- List<User> agents = idmManager.createIdentityQuery(User.class)
- .setParameter(User.EMAIL, email).getResultList();
- if (agents.isEmpty()) {
+ public static LDAPUser getUserByEmail(LDAPIdentityStore ldapIdentityStore, String email) {
+ IdentityQueryBuilder queryBuilder = ldapIdentityStore.createQueryBuilder();
+ IdentityQuery<LDAPUser> query = queryBuilder.createIdentityQuery(LDAPUser.class)
+ .where(queryBuilder.equal(LDAPUser.EMAIL, email));
+ List<LDAPUser> users = query.getResultList();
+
+ if (users.isEmpty()) {
return null;
- } else if (agents.size() == 1) {
- return agents.get(0);
+ } else if (users.size() == 1) {
+ return users.get(0);
} else {
- throw new IdentityManagementException("Error - multiple users found with same email");
+ throw new ModelDuplicateException("Error - multiple users found with same email " + email);
}
}
- public static boolean removeUser(PartitionManager partitionManager, String username) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- User picketlinkUser = BasicModel.getUser(idmManager, username);
- if (picketlinkUser == null) {
+ public static boolean removeUser(LDAPIdentityStore ldapIdentityStore, String username) {
+ LDAPUser ldapUser = getUser(ldapIdentityStore, username);
+ if (ldapUser == null) {
return false;
}
- idmManager.remove(picketlinkUser);
+ ldapIdentityStore.remove(ldapUser);
return true;
}
- public static void removeAllUsers(PartitionManager partitionManager) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- List<User> users = idmManager.createIdentityQuery(User.class).getResultList();
+ public static void removeAllUsers(LDAPIdentityStore ldapIdentityStore) {
+ List<LDAPUser> allUsers = getAllUsers(ldapIdentityStore);
- for (User user : users) {
- idmManager.remove(user);
+ for (LDAPUser user : allUsers) {
+ ldapIdentityStore.remove(user);
}
}
- public static List<User> getAllUsers(PartitionManager partitionManager) {
- IdentityManager idmManager = getIdentityManager(partitionManager);
- return idmManager.createIdentityQuery(User.class).getResultList();
- }
-
- private static IdentityManager getIdentityManager(PartitionManager partitionManager) {
- return partitionManager.createIdentityManager();
+ public static List<LDAPUser> getAllUsers(LDAPIdentityStore ldapIdentityStore) {
+ IdentityQuery<LDAPUser> userQuery = ldapIdentityStore.createQueryBuilder().createIdentityQuery(LDAPUser.class);
+ return userQuery.getResultList();
}
// Needed for ActiveDirectory updates
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 9a68f6a..4debf0e 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -28,52 +23,43 @@ public class WritableLDAPUserModelDelegate extends UserModelDelegate implements
@Override
public void setUsername(String username) {
- IdentityManager identityManager = provider.getIdentityManager();
-
- 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);
+ LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
+
+ 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();
-
- 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);
+ LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
+
+ 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();
-
- 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);
+ LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
+
+ 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();
-
- 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);
+ LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore();
+
+ 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/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
index bf76be7..acab3e0 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 0000000..fc3b538
--- /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 0000000..93688a4
--- /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 {
+
+ /**
+ * <p> Different options can be used to match a specific property based on its type. Regardless of the option
+ * chosen, if the property type equals the <code>propertyClass</code> it will be selected. <p/> <ul> <li>SUB_TYPE:
+ * Also consider properties where its type is a subtype of <code>propertyClass</code>. .</li> <li>SUPER_TYPE: Also
+ * consider properties where its type is a superclass or superinterface of <code>propertyClass</code>. .</li> </ul>
+ * </p>
+ */
+ public static enum MatchOption {
+ SUB_TYPE, SUPER_TYPE, ALL
+ }
+
+ private final Class<?> propertyClass;
+ private final MatchOption matchOption;
+
+ public TypedPropertyCriteria(Class<?> propertyClass) {
+ this(propertyClass, null);
+ }
+
+ public TypedPropertyCriteria(Class<?> propertyClass, MatchOption matchOption) {
+ if (propertyClass == null) {
+ throw new IllegalArgumentException("Property class can not be null.");
+ }
+ this.propertyClass = propertyClass;
+ this.matchOption = matchOption;
+ }
+
+ public boolean fieldMatches(Field f) {
+ return match(f.getType());
+ }
+
+ public boolean methodMatches(Method m) {
+ return match(m.getReturnType());
+ }
+
+ private boolean match(Class<?> type) {
+ if (propertyClass.equals(type)) {
+ return true;
+ } else {
+ boolean matchSubType = propertyClass.isAssignableFrom(type);
+
+ if (MatchOption.SUB_TYPE == this.matchOption) {
+ return matchSubType;
+ }
+
+ boolean matchSuperType = type.isAssignableFrom(propertyClass);
+
+ if (MatchOption.SUPER_TYPE == this.matchOption) {
+ return matchSuperType;
+ }
+
+ if (MatchOption.ALL == this.matchOption) {
+ return matchSubType || matchSuperType;
+ }
+ }
+
+ return false;
+ }
+}
+
pom.xml 1(+0 -1)
diff --git a/pom.xml b/pom.xml
index f3f68b0..87ac607 100755
--- a/pom.xml
+++ b/pom.xml
@@ -114,7 +114,6 @@
<module>model</module>
<module>integration</module>
<module>proxy</module>
- <module>picketlink</module>
<module>federation</module>
<module>services</module>
<module>saml</module>
services/pom.xml 6(+0 -6)
diff --git a/services/pom.xml b/services/pom.xml
index e5fde97..ea6e90e 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -103,12 +103,6 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-picketlink-api</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
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 cfaae07..929029e 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<String,String> ldapConfig = ldapRule.getConfig();
- ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "true");
+ ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
// Delete all LDAP users and add some new for testing
- PartitionManager partitionManager = getPartitionManager(manager.getSession(), ldapModel);
- LDAPUtils.removeAllUsers(partitionManager);
+ LDAPIdentityStore ldapStore = getLdapIdentityStore(manager.getSession(), ldapModel);
+ LDAPUtils.removeAllUsers(ldapStore);
- User john = LDAPUtils.addUser(partitionManager, "johnkeycloak", "John", "Doe", "john@email.org");
- LDAPUtils.updatePassword(partitionManager, john, "Password1");
+ LDAPUser john = LDAPUtils.addUser(ldapStore, "johnkeycloak", "John", "Doe", "john@email.org");
+ LDAPUtils.updatePassword(ldapStore, john, "Password1");
- User existing = LDAPUtils.addUser(partitionManager, "existing", "Existing", "Foo", "existing@email.org");
+ LDAPUser existing = LDAPUtils.addUser(ldapStore, "existing", "Existing", "Foo", "existing@email.org");
}
});
@@ -339,13 +338,13 @@ public class FederationProvidersIntegrationTest {
@Test
public void testSearch() {
KeycloakSession session = keycloakRule.startSession();
- PartitionManager partitionManager = getPartitionManager(session, ldapModel);
+ LDAPIdentityStore ldapStore = getLdapIdentityStore(session, ldapModel);
try {
RealmModel appRealm = session.realms().getRealmByName("test");
- LDAPUtils.addUser(partitionManager, "username1", "John1", "Doel1", "user1@email.org");
- LDAPUtils.addUser(partitionManager, "username2", "John2", "Doel2", "user2@email.org");
- LDAPUtils.addUser(partitionManager, "username3", "John3", "Doel3", "user3@email.org");
- LDAPUtils.addUser(partitionManager, "username4", "John4", "Doel4", "user4@email.org");
+ LDAPUtils.addUser(ldapStore, "username1", "John1", "Doel1", "user1@email.org");
+ LDAPUtils.addUser(ldapStore, "username2", "John2", "Doel2", "user2@email.org");
+ LDAPUtils.addUser(ldapStore, "username3", "John3", "Doel3", "user3@email.org");
+ LDAPUtils.addUser(ldapStore, "username4", "John4", "Doel4", "user4@email.org");
// Users are not at local store at this moment
Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm));
@@ -395,7 +394,7 @@ public class FederationProvidersIntegrationTest {
Assert.assertTrue(session.users().validCredentials(appRealm, user, cred));
// LDAP password is still unchanged
- Assert.assertTrue(LDAPUtils.validatePassword(getPartitionManager(session, model), "johnkeycloak", "Password1"));
+ Assert.assertTrue(LDAPUtils.validatePassword(getLdapIdentityStore(session, model), user, "Password1"));
// ATM it's not permitted to delete user in unsynced mode. Should be user deleted just locally instead?
Assert.assertFalse(session.users().removeUser(appRealm, user));
@@ -412,9 +411,10 @@ public class FederationProvidersIntegrationTest {
}
}
- static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
- PartitionManagerProvider partitionManagerProvider = keycloakSession.getProvider(PartitionManagerProvider.class);
- return partitionManagerProvider.getPartitionManager(ldapFedModel);
+ static LDAPIdentityStore getLdapIdentityStore(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
+ LDAPFederationProviderFactory ldapProviderFactory = (LDAPFederationProviderFactory) keycloakSession.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, ldapFedModel.getProviderName());
+ LDAPFederationProvider ldapProvider = ldapProviderFactory.getInstance(keycloakSession, ldapFedModel);
+ return ldapProvider.getLdapIdentityStore();
}
}
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 55f68bb..f628519 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<String,String> ldapConfig = ldapRule.getConfig();
- ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "false");
+ ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "false");
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap",
-1, -1, 0);
// Delete all LDAP users and add 5 new users for testing
- PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(manager.getSession(), ldapModel);
- LDAPUtils.removeAllUsers(partitionManager);
-
- 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");
+ LDAPIdentityStore ldapStore = FederationProvidersIntegrationTest.getLdapIdentityStore(manager.getSession(), ldapModel);
+ LDAPUtils.removeAllUsers(ldapStore);
+
+ for (int i=1 ; i<6 ; i++) {
+ LDAPUser user = LDAPUtils.addUser(ldapStore, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org");
+ LDAPUtils.updatePassword(ldapStore, user, "Password1");
+ }
// Add dummy provider
dummyModel = appRealm.addUserFederationProvider(DummyUserFederationProviderFactory.PROVIDER_NAME, new HashMap<String, String>(), 1, "test-dummy", -1, 1, 0);
@@ -122,9 +115,9 @@ public class SyncProvidersTest {
sleep(1000);
// Add user to LDAP and update 'user5' in LDAP
- PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(session, ldapModel);
- LDAPUtils.addUser(partitionManager, "user6", "User6FN", "User6LN", "user6@email.org");
- LDAPUtils.updateUser(partitionManager, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
+ LDAPIdentityStore ldapStore = FederationProvidersIntegrationTest.getLdapIdentityStore(session, ldapModel);
+ LDAPUtils.addUser(ldapStore, "user6", "User6FN", "User6LN", "user6@email.org");
+ LDAPUtils.updateUser(ldapStore, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
// Assert still old users in local provider
assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");