keycloak-aplcache
Changes
connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java 77(+39 -38)
connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java 13(+8 -5)
connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java 7(+0 -7)
examples/kerberos/README.md 11(+6 -5)
examples/kerberos/users.ldif 0(+0 -0)
examples/ldap/embedded-ldap/assembly.xml 21(+21 -0)
examples/ldap/embedded-ldap/pom.xml 82(+82 -0)
examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java 127(+127 -0)
examples/ldap/ldap-app/pom.xml 68(+68 -0)
examples/ldap/ldap-app/users.ldif 0(+0 -0)
examples/ldap/pom.xml 20(+20 -0)
examples/pom.xml 1(+1 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java 27(+18 -9)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java 131(+99 -32)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java 2(+1 -1)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java 37(+31 -6)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java 6(+5 -1)
pom.xml 11(+7 -4)
testsuite/integration/pom.xml 40(+4 -36)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java 35(+22 -13)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java 11(+7 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java 17(+17 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java 74(+68 -6)
testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/EmbeddedServersFactory.java 110(+0 -110)
testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPEmbeddedServer.java 196(+0 -196)
util/embedded-ldap/pom.xml 91(+91 -0)
util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java 267(+267 -0)
util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java 4(+2 -2)
util/pom.xml 22(+22 -0)
Details
diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index e66ad65..06c1bdc 100755
--- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -70,55 +70,56 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
Connection connection = null;
- String unitName = config.get("unitName");
String databaseSchema = config.get("databaseSchema");
Map<String, Object> properties = new HashMap<String, Object>();
- // Only load config from keycloak-server.json if unitName is not specified
- if (unitName == null) {
- unitName = "keycloak-default";
+ String unitName = "keycloak-default";
- String dataSource = config.get("dataSource");
- if (dataSource != null) {
- if (config.getBoolean("jta", false)) {
- properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
- } else {
- properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
- }
+ String dataSource = config.get("dataSource");
+ if (dataSource != null) {
+ if (config.getBoolean("jta", false)) {
+ properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
} else {
- properties.put(AvailableSettings.JDBC_URL, config.get("url"));
- properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
-
- String user = config.get("user");
- if (user != null) {
- properties.put(AvailableSettings.JDBC_USER, user);
- }
- String password = config.get("password");
- if (password != null) {
- properties.put(AvailableSettings.JDBC_PASSWORD, password);
- }
+ properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
}
+ } else {
+ properties.put(AvailableSettings.JDBC_URL, config.get("url"));
+ properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
- String driverDialect = config.get("driverDialect");
- if (driverDialect != null && driverDialect.length() > 0) {
- properties.put("hibernate.dialect", driverDialect);
+ String user = config.get("user");
+ if (user != null) {
+ properties.put(AvailableSettings.JDBC_USER, user);
}
-
- if (databaseSchema != null) {
- if (databaseSchema.equals("development-update")) {
- properties.put("hibernate.hbm2ddl.auto", "update");
- databaseSchema = null;
- } else if (databaseSchema.equals("development-validate")) {
- properties.put("hibernate.hbm2ddl.auto", "validate");
- databaseSchema = null;
- }
+ String password = config.get("password");
+ if (password != null) {
+ properties.put(AvailableSettings.JDBC_PASSWORD, password);
}
+ }
- properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
- properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
+ String driverDialect = config.get("driverDialect");
+ if (driverDialect != null && driverDialect.length() > 0) {
+ properties.put("hibernate.dialect", driverDialect);
}
+ String schema = config.get("schema");
+ if (schema != null) {
+ properties.put("hibernate.default_schema", schema);
+ }
+
+ if (databaseSchema != null) {
+ if (databaseSchema.equals("development-update")) {
+ properties.put("hibernate.hbm2ddl.auto", "update");
+ databaseSchema = null;
+ } else if (databaseSchema.equals("development-validate")) {
+ properties.put("hibernate.hbm2ddl.auto", "validate");
+ databaseSchema = null;
+ }
+ }
+
+ properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
+ properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
+
if (databaseSchema != null) {
logger.trace("Updating database");
@@ -140,12 +141,12 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
- updater.update(session, connection);
+ updater.update(session, connection, schema);
} else {
logger.debug("Database is up to date");
}
} else if (databaseSchema.equals("validate")) {
- updater.validate(connection);
+ updater.validate(connection, schema);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
index 809f2f3..4c67ada 100755
--- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
+++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
@@ -16,8 +16,8 @@ public interface JpaUpdaterProvider extends Provider {
public String getCurrentVersionSql();
- public void update(KeycloakSession session, Connection connection);
+ public void update(KeycloakSession session, Connection connection, String defaultSchema);
- public void validate(Connection connection);
+ public void validate(Connection connection, String defaultSchema);
}
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index a611c3c..65e09da 100644
--- a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -51,14 +51,14 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
@Override
- public void update(KeycloakSession session, Connection connection) {
+ public void update(KeycloakSession session, Connection connection, String defaultSchema) {
logger.debug("Starting database update");
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
ThreadLocalSessionContext.setCurrentSession(session);
try {
- Liquibase liquibase = getLiquibase(connection);
+ Liquibase liquibase = getLiquibase(connection, defaultSchema);
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
if (!changeSets.isEmpty()) {
@@ -93,9 +93,9 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
@Override
- public void validate(Connection connection) {
+ public void validate(Connection connection, String defaultSchema) {
try {
- Liquibase liquibase = getLiquibase(connection);
+ Liquibase liquibase = getLiquibase(connection, defaultSchema);
liquibase.validate();
} catch (Exception e) {
@@ -103,7 +103,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
}
- private Liquibase getLiquibase(Connection connection) throws Exception {
+ private Liquibase getLiquibase(Connection connection, String defaultSchema) throws Exception {
ServiceLocator sl = ServiceLocator.getInstance();
if (!System.getProperties().containsKey("liquibase.scan.packages")) {
@@ -125,6 +125,9 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
LogFactory.setInstance(new LogWrapper());
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+ if (defaultSchema != null) {
+ database.setDefaultSchemaName(defaultSchema);
+ }
return new Liquibase(CHANGELOG, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
}
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
index e592df9..b93e2b9 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
@@ -89,13 +89,6 @@ public class BasicDBObjectMapper<S> implements Mapper<BasicDBObject, S> {
Type[] genericTypeArguments = parameterized.getActualTypeArguments();
List<Type> genericTypes = Arrays.asList(genericTypeArguments);
- /*for (Type genericType : genericTypeArguments) {
- if (genericType instanceof Class) {
- genericTypes.add((Class<?>) genericType);
- } else {
- System.out.println("foo");
- }
- }*/
Class<?> expectedReturnType = (Class<?>)parameterized.getRawType();
context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes);
diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml
index 214d88a..fd75be0 100755
--- a/docbook/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/reference/en/en-US/modules/server-installation.xml
@@ -193,17 +193,14 @@
</listitem>
</varlistentry>
<varlistentry>
- <term>unitName</term>
+ <term>schema</term>
<listitem>
<para>
- Allow you to specify name of persistence unit if you want to provide your own persistence.xml file for JPA configuration.
- If this option is used, then all other configuration options are ignored as you are expected to configure
- all JPA/DB properties in your own persistence.xml file. Hence you can remove properties "dataSource" and "databaseSchema" in this case.
+ Specify the default database schema to use
</para>
</listitem>
</varlistentry>
</variablelist>
- For more info about Hibernate properties, see <ulink url="http://hibernate.org/orm/documentation/">Hibernate and JPA documentation</ulink> .
</para>
<section>
<title>Tested databases</title>
examples/kerberos/README.md 11(+6 -5)
diff --git a/examples/kerberos/README.md b/examples/kerberos/README.md
index 80e1ff8..5993a1f 100644
--- a/examples/kerberos/README.md
+++ b/examples/kerberos/README.md
@@ -19,7 +19,7 @@ cp http.keytab /tmp/http.keytab
```
Alternative is to configure different location for `keyTab` property in `kerberosrealm.json` configuration file (On Windows this will be needed).
-Note that in production, keytab file should be in secured location accessible just to the user under which is Keycloak server running.
+WARNING: In production, keytab file should be in secured location accessible just to the user under which is Keycloak server running.
**3)** Run Keycloak server and import `kerberosrealm.json` into it through admin console. This will import realm with sample application
@@ -37,12 +37,13 @@ Also if you are on Linux, make sure that record like:
```
is in your `/etc/hosts` before other records for the 127.0.0.1 host to avoid issues related to incompatible reverse lookup (Ensure the similar for other OS as well)
+**4)** Install kerberos client. This is platform dependent. If you are on Fedora, Ubuntu or RHEL, you can install package `freeipa-client`, which contains Kerberos client and bunch of other stuff.
-**4)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm and enable `forwardable` flag, which is needed
+**5)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm for host `localhost` and enable `forwardable` flag, which is needed
for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server.
See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/test/resources/kerberos/test-krb5.conf) for inspiration.
-**5)** Run ApacheDS based Kerberos server embedded in Keycloak. Easiest is to checkout keycloak sources, build and then run KerberosEmbeddedServer
+**6)** Run ApacheDS based Kerberos server embedded in Keycloak. Easiest is to checkout keycloak sources, build and then run KerberosEmbeddedServer
as shown here:
```
@@ -55,12 +56,12 @@ mvn exec:java -Pkerberos
More details about embedded Kerberos server in [testsuite README](https://github.com/keycloak/keycloak/blob/master/misc/Testsuite.md#kerberos-server).
-**6)** Configure browser (Firefox, Chrome or other) and enable SPNEGO authentication and credential delegation for `localhost` .
+**7)** Configure browser (Firefox, Chrome or other) and enable SPNEGO authentication and credential delegation for `localhost` .
In Firefox it can be done by adding `localhost` to both `network.negotiate-auth.trusted-uris` and `network.negotiate-auth.delegation-uris` .
More info in [testsuite README](https://github.com/keycloak/keycloak/blob/master/misc/Testsuite.md#kerberos-server).
-**7)** Test the example. Obtain kerberos ticket by running command from CMD (on linux):
+**8)** Test the example. Obtain kerberos ticket by running command from CMD (on linux):
```
kinit hnelson@KEYCLOAK.ORG
```
examples/kerberos/users.ldif 0(+0 -0)
diff --git a/examples/kerberos/users.ldif b/examples/kerberos/users.ldif
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/examples/kerberos/users.ldif
examples/ldap/embedded-ldap/assembly.xml 21(+21 -0)
diff --git a/examples/ldap/embedded-ldap/assembly.xml b/examples/ldap/embedded-ldap/assembly.xml
new file mode 100644
index 0000000..58afeca
--- /dev/null
+++ b/examples/ldap/embedded-ldap/assembly.xml
@@ -0,0 +1,21 @@
+<assembly>
+ <id>embedded-ldap</id>
+
+ <formats>
+ <format>dir</format>
+ </formats>
+
+ <includeBaseDirectory>false</includeBaseDirectory>
+
+ <dependencySets>
+ <dependencySet>
+ <unpack>false</unpack>
+ <useTransitiveDependencies>true</useTransitiveDependencies>
+ <useTransitiveFiltering>true</useTransitiveFiltering>
+ <includes>
+ <include>org.keycloak:keycloak-util-embedded-ldap</include>
+ <include>org.slf4j:slf4j-log4j12</include>
+ </includes>
+ </dependencySet>
+ </dependencySets>
+</assembly>
\ No newline at end of file
examples/ldap/embedded-ldap/pom.xml 82(+82 -0)
diff --git a/examples/ldap/embedded-ldap/pom.xml b/examples/ldap/embedded-ldap/pom.xml
new file mode 100644
index 0000000..b981072
--- /dev/null
+++ b/examples/ldap/embedded-ldap/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>keycloak-examples-ldap-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.4.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.keycloak.example.demo</groupId>
+ <artifactId>keycloak-examples-embedded-ldap</artifactId>
+ <packaging>jar</packaging>
+ <name>LDAP Demo Application</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-util-embedded-ldap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>embedded-ldap</finalName>
+ <plugins>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>assemble</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ <configuration>
+ <descriptors>
+ <descriptor>assembly.xml</descriptor>
+ </descriptors>
+ <outputDirectory>
+ target
+ </outputDirectory>
+ <workDirectory>
+ target/assembly/work
+ </workDirectory>
+ <appendAssemblyId>false</appendAssemblyId>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>org.keycloak.example.ldap.embedded.EmbeddedLDAPLauncher</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java b/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java
new file mode 100644
index 0000000..b3941d5
--- /dev/null
+++ b/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java
@@ -0,0 +1,127 @@
+package org.keycloak.example.ldap.embedded;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is supposed to be executed from JAR file (java -jar target/embedded-ldap.jar ). For executing from IDE or Maven use directly
+ * the proper class (LDAPEmbeddedServer, KerberosEmbeddedServer or KerberosKeytabCreator)
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class EmbeddedLDAPLauncher {
+
+ public static void main(String[] args) throws Exception {
+ String arg = args.length == 0 ? null : args[0];
+ if (arg == null) {
+ System.err.println("Missing argument: either 'kerberos', 'ldap' or 'keytabCreator' must be passed as argument");
+ System.exit(1);
+ }
+
+ String clazz = null;
+ File home = getHome();
+ Properties defaultProperties = new Properties();
+ if (arg.equalsIgnoreCase("ldap")) {
+
+ clazz = "org.keycloak.util.ldap.LDAPEmbeddedServer";
+ File ldapLdif = file(home, "..", "ldap-app", "users.ldif");
+ defaultProperties.put("ldap.ldif", ldapLdif.getAbsolutePath());
+ } else if (arg.equalsIgnoreCase("kerberos")) {
+
+ clazz = "org.keycloak.util.ldap.KerberosEmbeddedServer";
+ File kerberosLdif = file(home, "..", "..", "kerberos", "users.ldif");
+ defaultProperties.put("ldap.ldif", kerberosLdif.getAbsolutePath());
+ } else if (arg.equalsIgnoreCase("keytabCreator")) {
+
+ clazz = "org.keycloak.util.ldap.KerberosKeytabCreator";
+ } else {
+
+ System.err.println("Invalid argument: '" + arg + "' . Either 'kerberos', 'ldap' or 'keytabCreator' must be passed as argument");
+ System.exit(1);
+ }
+
+ // Remove first argument
+ String[] newArgs = new String[args.length - 1];
+ for (int i=0 ; i<(args.length - 1) ; i++) {
+ newArgs[i] = args[i + 1];
+ }
+
+ System.out.println("Executing " + clazz);
+ runClass(clazz, newArgs, defaultProperties);
+ }
+
+
+ private static void runClass(String className, String[] args, Properties defaultProperties) throws Exception {
+ File home = getHome();
+ File lib = file(home, "target", "embedded-ldap");
+
+ if (!lib.exists()) {
+ System.err.println("Could not find lib directory: " + lib.toString());
+ System.exit(1);
+ } else {
+ System.out.println("Found directory to load jars: " + lib.getAbsolutePath());
+ }
+
+ List<URL> jars = new ArrayList<URL>();
+ for (File file : lib.listFiles()) {
+ jars.add(file.toURI().toURL());
+ }
+ URL[] urls = jars.toArray(new URL[jars.size()]);
+ URLClassLoader loader = new URLClassLoader(urls, EmbeddedLDAPLauncher.class.getClassLoader());
+
+ Class mainClass = loader.loadClass(className);
+ Method executeMethod = null;
+ for (Method m : mainClass.getMethods()) if (m.getName().equals("execute")) { executeMethod = m; break; }
+ Object obj = args;
+ executeMethod.invoke(null, obj, defaultProperties);
+ }
+
+
+ private static File getHome() {
+ String launcherPath = EmbeddedLDAPLauncher.class.getName().replace('.', '/') + ".class";
+ URL jarfile = EmbeddedLDAPLauncher.class.getClassLoader().getResource(launcherPath);
+ if (jarfile != null) {
+ Matcher m = Pattern.compile("jar:(file:.*)!/" + launcherPath).matcher(jarfile.toString());
+ if (m.matches()) {
+ try {
+ File jarPath = new File(new URI(m.group(1)));
+ File libPath = jarPath.getParentFile().getParentFile();
+ System.out.println("Home directory: " + libPath.toString());
+ if (!libPath.exists()) {
+ System.exit(1);
+
+ }
+ return libPath;
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ } else {
+ System.err.println("jar file null: " + launcherPath);
+ }
+ return null;
+ }
+
+ private static File file(File home, String... pathItems) {
+ File current = home;
+
+ for (String item : pathItems) {
+ if (item.equals("..")) {
+ current = current.getParentFile();
+ } else {
+ current = new File(current, item);
+ }
+ }
+ return current;
+ }
+}
examples/ldap/ldap-app/pom.xml 68(+68 -0)
diff --git a/examples/ldap/ldap-app/pom.xml b/examples/ldap/ldap-app/pom.xml
new file mode 100644
index 0000000..d080ba6
--- /dev/null
+++ b/examples/ldap/ldap-app/pom.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>keycloak-examples-ldap-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.4.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.keycloak.example.demo</groupId>
+ <artifactId>keycloak-examples-ldap-app</artifactId>
+ <packaging>war</packaging>
+ <name>LDAP Demo Application</name>
+
+ <repositories>
+ <repository>
+ <id>jboss</id>
+ <name>jboss repo</name>
+ <url>http://repository.jboss.org/nexus/content/groups/public/</url>
+ </repository>
+ </repositories>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-adapter-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>ldap-portal</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.jboss.as.plugins</groupId>
+ <artifactId>jboss-as-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.wildfly.plugins</groupId>
+ <artifactId>wildfly-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
examples/ldap/ldap-app/users.ldif 0(+0 -0)
diff --git a/examples/ldap/ldap-app/users.ldif b/examples/ldap/ldap-app/users.ldif
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/examples/ldap/ldap-app/users.ldif
examples/ldap/pom.xml 20(+20 -0)
diff --git a/examples/ldap/pom.xml b/examples/ldap/pom.xml
new file mode 100644
index 0000000..d506efc
--- /dev/null
+++ b/examples/ldap/pom.xml
@@ -0,0 +1,20 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-examples-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.4.0.Final-SNAPSHOT</version>
+ </parent>
+ <name>Keycloak LDAP Examples - Parent</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-examples-ldap-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>embedded-ldap</module>
+ <module>ldap-app</module>
+ </modules>
+
+</project>
\ No newline at end of file
examples/pom.xml 1(+1 -0)
diff --git a/examples/pom.xml b/examples/pom.xml
index b2f7f2a..4815c39 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -44,5 +44,6 @@
<module>kerberos</module>
<module>themes</module>
<module>saml</module>
+ <module>ldap</module>
</modules>
</project>
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
index 67e92c7..141ae38 100644
--- 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
@@ -3,6 +3,7 @@ package org.keycloak.federation.ldap.idm.store.ldap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
@@ -437,18 +438,26 @@ public class LDAPIdentityStore implements IdentityStore {
// ldapObject.getReadOnlyAttributeNames() are lower-cased
if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
- BasicAttribute attr = new BasicAttribute(attrName);
+
if (attrValue == null) {
- // Adding empty value as we don't know if attribute is mandatory in LDAP
- attr.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
- } else {
- for (String val : attrValue) {
- if (val == null || val.toString().trim().length() == 0) {
- val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
- }
- attr.add(val);
+ // Shouldn't happen
+ logger.warnf("Attribute '%s' is null on LDAP object '%s' . Using empty value to be saved to LDAP", attrName, ldapObject.getDn().toString());
+ attrValue = Collections.emptySet();
+ }
+
+ // Ignore empty attributes during create
+ if (isCreate && attrValue.isEmpty()) {
+ continue;
+ }
+
+ BasicAttribute attr = new BasicAttribute(attrName);
+ for (String val : attrValue) {
+ if (val == null || val.toString().trim().length() == 0) {
+ val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
}
+ attr.add(val);
}
+
entryAttributes.put(attr);
}
}
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 66665bf..b5598e9 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
@@ -13,7 +13,9 @@ import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -26,6 +28,7 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserFederationSyncResult;
import org.keycloak.models.UserModel;
import org.keycloak.constants.KerberosConstants;
+import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -176,7 +179,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
for (LDAPObject ldapUser : ldapUsers) {
String ldapUsername = LDAPUtils.getUsername(ldapUser, this.ldapIdentityStore.getConfig());
if (session.userStorage().getUserByUsername(ldapUsername, realm) == null) {
- UserModel imported = importUserFromLDAP(realm, ldapUser);
+ UserModel imported = importUserFromLDAP(session, realm, ldapUser);
searchResults.add(imported);
}
}
@@ -249,10 +252,10 @@ public class LDAPFederationProvider implements UserFederationProvider {
return null;
}
- return importUserFromLDAP(realm, ldapUser);
+ return importUserFromLDAP(session, realm, ldapUser);
}
- protected UserModel importUserFromLDAP(RealmModel realm, LDAPObject ldapUser) {
+ protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) {
String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
if (ldapUsername == null) {
@@ -298,7 +301,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
return null;
}
- return importUserFromLDAP(realm, ldapUser);
+ return importUserFromLDAP(session, realm, ldapUser);
}
@Override
@@ -383,38 +386,6 @@ public class LDAPFederationProvider implements UserFederationProvider {
public void close() {
}
- protected UserFederationSyncResult importLDAPUsers(RealmModel realm, List<LDAPObject> ldapUsers, UserFederationProviderModel fedModel) {
- UserFederationSyncResult syncResult = new UserFederationSyncResult();
-
- for (LDAPObject ldapUser : ldapUsers) {
- String username = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
- UserModel currentUser = session.userStorage().getUserByUsername(username, realm);
-
- if (currentUser == null) {
- // Add new user to Keycloak
- importUserFromLDAP(realm, ldapUser);
- syncResult.increaseAdded();
- } else {
- if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
-
- // Update keycloak user
- Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
- for (UserFederationMapperModel mapperModel : federationMappers) {
- LDAPFederationMapper ldapMapper = getMapper(mapperModel);
- ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, currentUser, realm, false);
- }
-
- logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
- syncResult.increaseUpdated();
- } else {
- logger.warnf("User '%s' is not updated during sync as he is not linked to federation provider '%s'", username, fedModel.getDisplayName());
- }
- }
- }
-
- return syncResult;
- }
-
/**
* Called after successful kerberos authentication
*
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 2b60ff8..a71b84d 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
@@ -14,12 +14,15 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
+import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationEventAwareProviderFactory;
import org.keycloak.models.UserFederationMapperModel;
@@ -94,7 +97,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
// CN is typically used as RDN for Active Directory deployments
@@ -107,7 +111,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
} else {
@@ -118,14 +123,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username-cn", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
} else {
@@ -141,7 +148,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
}
@@ -149,14 +157,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addUserFederationMapper(mapperModel);
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
@@ -167,7 +177,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
UserAttributeLDAPFederationMapper.READ_ONLY, "true",
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
// map modifyTimeStamp as read-only
@@ -175,7 +186,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
UserAttributeLDAPFederationMapper.READ_ONLY, "true",
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
}
@@ -226,29 +238,14 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
userQuery.setLimit(pageSize);
final List<LDAPObject> users = userQuery.getResultList();
nextPage = userQuery.getPaginationContext() != null;
-
- KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
-
- @Override
- public void run(KeycloakSession session) {
- UserFederationSyncResult currentPageSync = importLdapUsers(session, realmId, fedModel, users);
- syncResult.add(currentPageSync);
- }
-
- });
+ UserFederationSyncResult currentPageSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+ syncResult.add(currentPageSync);
}
} else {
// LDAP pagination not available. Do everything in single transaction
final List<LDAPObject> users = userQuery.getResultList();
- KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
-
- @Override
- public void run(KeycloakSession session) {
- UserFederationSyncResult currentSync = importLdapUsers(session, realmId, fedModel, users);
- syncResult.add(currentSync);
- }
-
- });
+ UserFederationSyncResult currentSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+ syncResult.add(currentSync);
}
return syncResult;
@@ -273,11 +270,81 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
return queryHolder.query;
}
+ protected UserFederationSyncResult importLdapUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel fedModel, List<LDAPObject> ldapUsers) {
+ final UserFederationSyncResult syncResult = new UserFederationSyncResult();
+
+ class BooleanHolder {
+ private boolean value = true;
+ }
+ final BooleanHolder exists = new BooleanHolder();
+
+ for (final LDAPObject ldapUser : ldapUsers) {
+
+ try {
+
+ // Process each user in it's own transaction to avoid global fail
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
+ RealmModel currentRealm = session.realms().getRealm(realmId);
+
+ String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+ UserModel currentUser = session.userStorage().getUserByUsername(username, currentRealm);
+
+ if (currentUser == null) {
+
+ // Add new user to Keycloak
+ exists.value = false;
+ ldapFedProvider.importUserFromLDAP(session, currentRealm, ldapUser);
+ syncResult.increaseAdded();
+
+ } else {
+ if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
+
+ // Update keycloak user
+ Set<UserFederationMapperModel> federationMappers = currentRealm.getUserFederationMappersByFederationProvider(fedModel.getId());
+ for (UserFederationMapperModel mapperModel : federationMappers) {
+ LDAPFederationMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
+ ldapMapper.onImportUserFromLDAP(mapperModel, ldapFedProvider, ldapUser, currentUser, currentRealm, false);
+ }
+
+ logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
+ syncResult.increaseUpdated();
+ } else {
+ logger.warnf("User '%s' is not updated during sync as he already exists in Keycloak database but is not linked to federation provider '%s'", username, fedModel.getDisplayName());
+ syncResult.increaseFailed();
+ }
+ }
+ }
+
+ });
+ } catch (ModelException me) {
+ logger.error("Failed during import user from LDAP", me);
+ syncResult.increaseFailed();
+
+ // Remove user if we already added him during this transaction
+ if (!exists.value) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
+ RealmModel currentRealm = session.realms().getRealm(realmId);
+ String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+ UserModel existing = session.userStorage().getUserByUsername(username, currentRealm);
+ if (existing != null) {
+ session.userStorage().removeUser(currentRealm, existing);
+ }
+ }
+
+ });
+ }
+ }
+ }
- protected UserFederationSyncResult importLdapUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<LDAPObject> ldapUsers) {
- RealmModel realm = session.realms().getRealm(realmId);
- LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
- return ldapFedProvider.importLDAPUsers(realm, ldapUsers, fedModel);
+ return syncResult;
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
index 4565f88..e29ab6f 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -239,7 +239,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
memberships.remove(ldapUser.getDn().toString());
- // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But on active directory! (Empty membership is not allowed here)
+ // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Empty membership is not allowed here)
if (memberships.size() == 0 && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
index 24bf450..283089a 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
@@ -3,6 +3,7 @@ package org.keycloak.federation.ldap.mappers;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -15,6 +16,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
@@ -58,6 +60,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
public static final String LDAP_ATTRIBUTE = "ldap.attribute";
public static final String READ_ONLY = "read.only";
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
+ public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
@Override
@@ -88,6 +91,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
+ boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
@@ -95,15 +99,27 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
// we have java property on UserModel. Assuming we support just properties of simple types
Object attrValue = userModelProperty.getValue(localUser);
- String valueAsString = (attrValue == null) ? null : attrValue.toString();
- ldapUser.setSingleAttribute(ldapAttrName, valueAsString);
+
+ if (attrValue == null) {
+ if (isMandatoryInLdap) {
+ ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+ } else {
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
+ }
+ } else {
+ ldapUser.setSingleAttribute(ldapAttrName, attrValue.toString());
+ }
} else {
// we don't have java property. Let's set attribute
List<String> attrValues = localUser.getAttribute(userModelAttrName);
if (attrValues.size() == 0) {
- ldapUser.setAttribute(ldapAttrName, null);
+ if (isMandatoryInLdap) {
+ ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+ } else {
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
+ }
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(attrValues));
}
@@ -119,6 +135,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
+ final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
// For writable mode, we want to propagate writing of attribute to LDAP as well
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
@@ -170,12 +187,20 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
ensureTransactionStarted();
if (value == null) {
- ldapUser.setAttribute(ldapAttrName, null);
+ if (isMandatoryInLdap) {
+ ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+ } else {
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
+ }
} else if (value instanceof String) {
ldapUser.setSingleAttribute(ldapAttrName, (String) value);
} else {
List<String> asList = (List<String>) value;
- ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
+ if (asList.isEmpty() && isMandatoryInLdap) {
+ ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+ } else {
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
+ }
}
}
}
@@ -203,7 +228,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
if (name.equalsIgnoreCase(userModelAttrName)) {
Collection<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
if (ldapAttrValue == null) {
- return null;
+ return Collections.emptyList();
} else {
return new ArrayList<>(ldapAttrValue);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
index 1b1b44d..c14d0e8 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -31,9 +31,13 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
configProperties.add(readOnly);
- ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always read value from LDAP",
+ ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP",
"If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false");
configProperties.add(alwaysReadValueFromLDAP);
+
+ ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP",
+ "If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, "false");
+ configProperties.add(isMandatoryInLdap);
}
@Override
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 7d9e7ca..8bca31c 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -128,7 +128,7 @@ public class UserFederationManager implements UserProvider {
if (link != null) {
UserModel validatedProxyUser = link.validateAndProxy(realm, user);
if (validatedProxyUser != null) {
- managedUsers.put(user.getId(), user);
+ managedUsers.put(user.getId(), validatedProxyUser);
return validatedProxyUser;
} else {
deleteInvalidUser(realm, user);
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java b/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java
index e6d465b..b06348d 100644
--- a/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java
@@ -8,6 +8,7 @@ public class UserFederationSyncResult {
private int added;
private int updated;
private int removed;
+ private int failed;
public int getAdded() {
return added;
@@ -33,6 +34,14 @@ public class UserFederationSyncResult {
this.removed = removed;
}
+ public int getFailed() {
+ return failed;
+ }
+
+ public void setFailed(int failed) {
+ this.failed = failed;
+ }
+
public void increaseAdded() {
added++;
}
@@ -45,14 +54,23 @@ public class UserFederationSyncResult {
removed++;
}
+ public void increaseFailed() {
+ failed++;
+ }
+
public void add(UserFederationSyncResult other) {
added += other.added;
updated += other.updated;
removed += other.removed;
+ failed += other.failed;
}
public String getStatus() {
- return String.format("%d imported users, %d updated users, %d removed users", added, updated, removed);
+ String status = String.format("%d imported users, %d updated users, %d removed users", added, updated, removed);
+ if (failed != 0) {
+ status += String.format(", %d users failed sync! See server log for more details", failed);
+ }
+ return status;
}
@Override
pom.xml 11(+7 -4)
diff --git a/pom.xml b/pom.xml
index 33794fa..ca9d823 100755
--- a/pom.xml
+++ b/pom.xml
@@ -155,6 +155,7 @@
<module>testsuite</module>
<module>timer</module>
<module>export-import</module>
+ <module>util</module>
</modules>
<dependencyManagement>
@@ -431,25 +432,21 @@
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-annotations</artifactId>
<version>${apacheds.version}</version>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-interceptor-kerberos</artifactId>
<version>${apacheds.version}</version>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-annotations</artifactId>
<version>${apacheds.version}</version>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-ldap-codec-standalone</artifactId>
<version>${apacheds.codec.version}</version>
- <scope>test</scope>
</dependency>
<!-- Selenium -->
@@ -1122,6 +1119,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-util-embedded-ldap</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-docs-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
@@ -1184,6 +1186,7 @@
<version>${project.version}</version>
<classifier>classes</classifier>
</dependency>
+
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>federation-properties-example</artifactId>
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
index 75f6096..c39bc8a 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
@@ -1,5 +1,6 @@
package org.keycloak.services;
+import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakTransactionManager;
@@ -11,6 +12,8 @@ import java.util.List;
*/
public class DefaultKeycloakTransactionManager implements KeycloakTransactionManager {
+ public static final Logger logger = Logger.getLogger(DefaultKeycloakTransactionManager.class);
+
private List<KeycloakTransaction> transactions = new LinkedList<KeycloakTransaction>();
private List<KeycloakTransaction> afterCompletion = new LinkedList<KeycloakTransaction>();
private boolean active;
@@ -57,13 +60,26 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
exception = exception == null ? e : exception;
}
}
- for (KeycloakTransaction tx : afterCompletion) {
- try {
- tx.commit();
- } catch (RuntimeException e) {
- exception = exception == null ? e : exception;
+
+ // Don't commit "afterCompletion" if commit of some main transaction failed
+ if (exception == null) {
+ for (KeycloakTransaction tx : afterCompletion) {
+ try {
+ tx.commit();
+ } catch (RuntimeException e) {
+ exception = exception == null ? e : exception;
+ }
+ }
+ } else {
+ for (KeycloakTransaction tx : afterCompletion) {
+ try {
+ tx.rollback();
+ } catch (RuntimeException e) {
+ logger.error("Exception during rollback", e);
+ }
}
}
+
active = false;
if (exception != null) {
throw exception;
diff --git a/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java b/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java
index 1da7bfb..7b6b3ed 100644
--- a/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java
+++ b/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java
@@ -29,7 +29,13 @@ public class AdminMessagesProvider implements MessagesProvider {
@Override
public String getMessage(String messageKey, Object... parameters) {
String message = messagesBundle.getProperty(messageKey, messageKey);
- return new MessageFormat(message, locale).format(parameters);
+
+ try {
+ return new MessageFormat(message, locale).format(parameters);
+ } catch (Exception e) {
+ logger.warnf("Failed to format message due to: %s", e.getMessage());
+ return message;
+ }
}
@Override
testsuite/integration/pom.xml 40(+4 -36)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 0673780..cd7c98d 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -190,40 +190,8 @@
<!-- Apache DS -->
<dependency>
- <groupId>org.apache.directory.server</groupId>
- <artifactId>apacheds-core-annotations</artifactId>
- <exclusions>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.directory.jdbm</groupId>
- <artifactId>apacheds-jdbm1</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.apache.directory.server</groupId>
- <artifactId>apacheds-interceptor-kerberos</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.directory.server</groupId>
- <artifactId>apacheds-server-annotations</artifactId>
- <exclusions>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.directory.jdbm</groupId>
- <artifactId>apacheds-jdbm1</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.apache.directory.api</groupId>
- <artifactId>api-ldap-codec-standalone</artifactId>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-util-embedded-ldap</artifactId>
</dependency>
<dependency>
@@ -344,7 +312,7 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
- <mainClass>org.keycloak.testsuite.ldap.LDAPEmbeddedServer</mainClass>
+ <mainClass>org.keycloak.util.ldap.LDAPEmbeddedServer</mainClass>
<classpathScope>test</classpathScope>
</configuration>
</plugin>
@@ -359,7 +327,7 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
- <mainClass>org.keycloak.testsuite.ldap.KerberosEmbeddedServer</mainClass>
+ <mainClass>org.keycloak.util.ldap.KerberosEmbeddedServer</mainClass>
<classpathScope>test</classpathScope>
</configuration>
</plugin>
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 c4a028c..1945701 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
@@ -68,10 +68,10 @@ public class FederationProvidersIntegrationTest {
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
- LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234");
+ LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1");
- LDAPObject existing = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", "5678");
+ LDAPObject existing = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
}
});
@@ -270,7 +270,7 @@ public class FederationProvidersIntegrationTest {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", "12398");
+ LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", null, "12398");
// Remove default zipcode mapper and add the mapper for "POstalCode" to test case sensitivity
UserFederationMapperModel currentZipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper");
@@ -295,7 +295,7 @@ public class FederationProvidersIntegrationTest {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", "12399");
+ LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", null, "12399");
// Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername("johndirect", appRealm);
@@ -307,9 +307,18 @@ public class FederationProvidersIntegrationTest {
johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
ldapFedProvider.getLdapIdentityStore().update(johnDirect);
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ session = keycloakRule.startSession();
+ try {
+ RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("johndirect", appRealm);
+
// Verify that postalCode is still the same as we read it's value from Keycloak DB
user = session.users().getUserByUsername("johndirect", appRealm);
- postalCode = user.getFirstAttribute("postal_code");
+ String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12399", postalCode);
// Check user.getAttributes()
@@ -370,7 +379,7 @@ public class FederationProvidersIntegrationTest {
// Add the user with some fullName into LDAP directly. Ensure that fullName is saved into "cn" attribute in LDAP (currently mapped to model firstName)
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "fullname", "James Dee", "Dee", "fullname@email.org", "4578");
+ FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "fullname", "James Dee", "Dee", "fullname@email.org", null, "4578");
// add fullname mapper to the provider and remove "firstNameMapper". For this test, we will simply map full name to the LDAP attribute, which was before firstName ( "givenName" on active directory, "cn" on other LDAP servers)
firstNameMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "first name");
@@ -381,9 +390,6 @@ public class FederationProvidersIntegrationTest {
FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, ldapFirstNameAttributeName,
UserAttributeLDAPFederationMapper.READ_ONLY, "false");
appRealm.addUserFederationMapper(fullNameMapperModel);
-
- // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
- FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578");
} finally {
keycloakRule.stopSession(session, true);
}
@@ -392,6 +398,9 @@ public class FederationProvidersIntegrationTest {
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+ // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
+ FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578");
+
// Remove "fullnameUser" to assert he is removed from LDAP. Revert mappers to previous state
UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm);
session.users().removeUser(appRealm, fullnameUser);
@@ -485,10 +494,10 @@ public class FederationProvidersIntegrationTest {
RealmModel appRealm = session.realms().getRealmByName("test");
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username1", "John1", "Doel1", "user1@email.org", "121");
- FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username2", "John2", "Doel2", "user2@email.org", "122");
- FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username3", "John3", "Doel3", "user3@email.org", "123");
- FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", "124");
+ FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username1", "John1", "Doel1", "user1@email.org", null, "121");
+ FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username2", "John2", "Doel2", "user2@email.org", null, "122");
+ FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username3", "John3", "Doel3", "user3@email.org", null, "123");
+ FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", null, "124");
// Users are not at local store at this moment
Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index 56c4a70..939fe24 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
@@ -34,7 +34,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
class FederationTestUtils {
public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
- UserModel user = session.users().addUser(realm, username);
+ UserModel user = session.userStorage().addUser(realm, username);
user.setEmail(email);
user.setEnabled(true);
@@ -47,7 +47,7 @@ class FederationTestUtils {
}
public static LDAPObject addLDAPUser(LDAPFederationProvider ldapProvider, RealmModel realm, final String username,
- final String firstName, final String lastName, final String email, final String postalCode) {
+ final String firstName, final String lastName, final String email, final String street, final String... postalCode) {
UserModel helperUser = new UserModelDelegate(null) {
@Override
@@ -72,8 +72,10 @@ class FederationTestUtils {
@Override
public List<String> getAttribute(String name) {
- if ("postal_code".equals(name)) {
+ if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
return Arrays.asList(postalCode);
+ } else if ("street".equals(name) && street != null) {
+ return Arrays.asList(street);
} else {
return Collections.emptyList();
}
@@ -105,7 +107,8 @@ class FederationTestUtils {
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, ldapAttributeName,
UserAttributeLDAPFederationMapper.READ_ONLY, "false",
- UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+ UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
index e1e9aff..edd210c 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
@@ -1,6 +1,9 @@
package org.keycloak.testsuite.federation;
import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -17,6 +20,7 @@ import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
@@ -60,6 +64,19 @@ public class LDAPMultipleAttributesTest {
FederationTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET);
+ // Remove current users and add default users
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+ FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+ LDAPObject james = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", null, "88441");
+ ldapFedProvider.getLdapIdentityStore().updatePassword(james, "password");
+
+ // User for testing duplicating surname and postalCode
+ LDAPObject bruce = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "bwilson", "Bruce", "Wilson", "bwilson@keycloak.org", "Elm 5", "88441", "77332");
+ bruce.setAttribute("sn", new LinkedHashSet<>(Arrays.asList("Wilson", "Schneider")));
+ ldapFedProvider.getLdapIdentityStore().update(bruce);
+ ldapFedProvider.getLdapIdentityStore().updatePassword(bruce, "password");
+
// Create ldap-portal client
ClientModel ldapClient = appRealm.addClient("ldap-portal");
ldapClient.addRedirectUri("/ldap-portal");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
index 4802105..c0a8cd6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
@@ -76,13 +76,13 @@ public class LDAPRoleMappingsTest {
FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper");
// Add some users for testing
- LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234");
+ LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1");
- LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", "5678");
+ LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678");
ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1");
- LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", "8910");
+ LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910");
ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1");
// Add some roles for testing
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 b463bcd..e29a2b8 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
@@ -61,7 +61,7 @@ public class SyncProvidersTest {
FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
for (int i=1 ; i<=5 ; i++) {
- LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", "12" + i);
+ LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i);
ldapFedProvider.getLdapIdentityStore().updatePassword(ldapUser, "Password1");
}
@@ -81,7 +81,7 @@ public class SyncProvidersTest {
// }
@Test
- public void testLDAPSync() {
+ public void test01LDAPSync() {
UsersSyncManager usersSyncManager = new UsersSyncManager();
// wait a bit
@@ -91,7 +91,7 @@ public class SyncProvidersTest {
try {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
UserFederationSyncResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel);
- assertSyncEquals(syncResult, 5, 0, 0);
+ assertSyncEquals(syncResult, 5, 0, 0, 0);
} finally {
keycloakRule.stopSession(session, false);
}
@@ -123,7 +123,7 @@ public class SyncProvidersTest {
// Add user to LDAP and update 'user5' in LDAP
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126");
+ FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", null, "126");
LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5");
// NOTE: Changing LDAP attributes directly here
ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
@@ -137,7 +137,7 @@ public class SyncProvidersTest {
// Trigger partial sync
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
UserFederationSyncResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel);
- assertSyncEquals(syncResult, 1, 1, 0);
+ assertSyncEquals(syncResult, 1, 1, 0, 0);
} finally {
keycloakRule.stopSession(session, false);
}
@@ -155,6 +155,67 @@ public class SyncProvidersTest {
}
@Test
+ public void test02duplicateUsernameSync() {
+ LDAPObject duplicatedLdapUser;
+
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmModel testRealm = session.realms().getRealm("test");
+
+ FederationTestUtils.addLocalUser(session, testRealm, "user7", "user7@email.org", "password");
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+
+ // Add user to LDAP with duplicated username "user7"
+ duplicatedLdapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7", "User7FN", "User7LN", "user7-something@email.org", null, "126");
+
+ // Add user to LDAP with duplicated email "user7@email.org"
+ //FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7-something", "User7FNN", "User7LNL", "user7@email.org", null, "126");
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ session = keycloakRule.startSession();
+ try {
+ RealmModel testRealm = session.realms().getRealm("test");
+
+ // Assert syncing from LDAP fails due to duplicated username
+ UserFederationSyncResult result = new UsersSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+ Assert.assertEquals(1, result.getFailed());
+
+ // Remove "user7" from LDAP
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+ ldapFedProvider.getLdapIdentityStore().remove(duplicatedLdapUser);
+
+ // Add user to LDAP with duplicated email "user7@email.org"
+ duplicatedLdapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7-something", "User7FNN", "User7LNL", "user7@email.org", null, "126");
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ session = keycloakRule.startSession();
+ try {
+ RealmModel testRealm = session.realms().getRealm("test");
+
+ // Assert syncing from LDAP fails due to duplicated email
+ UserFederationSyncResult result = new UsersSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+ Assert.assertEquals(1, result.getFailed());
+ Assert.assertNull(session.userStorage().getUserByUsername("user7-something", testRealm));
+
+ // Update LDAP user to avoid duplicated email
+ duplicatedLdapUser.setSingleAttribute(LDAPConstants.EMAIL, "user7-changed@email.org");
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+ ldapFedProvider.getLdapIdentityStore().update(duplicatedLdapUser);
+
+ // Assert user successfully synced now
+ result = new UsersSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+ Assert.assertEquals(0, result.getFailed());
+ FederationTestUtils.assertUserImported(session.userStorage(), testRealm, "user7-something", "User7FNN", "User7LNL", "user7-changed@email.org", "126");
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+ @Test
public void testPeriodicSync() {
KeycloakSession session = keycloakRule.startSession();
try {
@@ -193,9 +254,10 @@ public class SyncProvidersTest {
}
}
- private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved) {
+ private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) {
Assert.assertEquals(syncResult.getAdded(), expectedAdded);
Assert.assertEquals(syncResult.getUpdated(), expectedUpdated);
Assert.assertEquals(syncResult.getRemoved(), expectedRemoved);
+ Assert.assertEquals(syncResult.getFailed(), expectedFailed);
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
index 9026557..57bf79a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
@@ -2,11 +2,12 @@ package org.keycloak.testsuite.rule;
import java.io.File;
import java.net.URL;
+import java.util.Properties;
import org.jboss.logging.Logger;
-import org.keycloak.testsuite.ldap.EmbeddedServersFactory;
-import org.keycloak.testsuite.ldap.LDAPTestConfiguration;
-import org.keycloak.testsuite.ldap.LDAPEmbeddedServer;
+import org.keycloak.testsuite.federation.LDAPTestConfiguration;
+import org.keycloak.util.ldap.KerberosEmbeddedServer;
+import org.keycloak.util.ldap.LDAPEmbeddedServer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -33,7 +34,11 @@ public class KerberosRule extends LDAPRule {
}
@Override
- protected LDAPEmbeddedServer createServer(EmbeddedServersFactory factory) {
- return factory.createKerberosServer();
+ protected LDAPEmbeddedServer createServer() {
+ Properties defaultProperties = new Properties();
+ defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY);
+ defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:kerberos/users-kerberos.ldif");
+
+ return new KerberosEmbeddedServer(defaultProperties);
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
index 1bf9b8e..add9708 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
@@ -1,11 +1,11 @@
package org.keycloak.testsuite.rule;
import java.util.Map;
+import java.util.Properties;
import org.junit.rules.ExternalResource;
-import org.keycloak.testsuite.ldap.EmbeddedServersFactory;
-import org.keycloak.testsuite.ldap.LDAPTestConfiguration;
-import org.keycloak.testsuite.ldap.LDAPEmbeddedServer;
+import org.keycloak.testsuite.federation.LDAPTestConfiguration;
+import org.keycloak.util.ldap.LDAPEmbeddedServer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -23,8 +23,7 @@ public class LDAPRule extends ExternalResource {
ldapTestConfiguration = LDAPTestConfiguration.readConfiguration(connectionPropsLocation);
if (ldapTestConfiguration.isStartEmbeddedLdapLerver()) {
- EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration();
- ldapEmbeddedServer = createServer(factory);
+ ldapEmbeddedServer = createServer();
ldapEmbeddedServer.init();
ldapEmbeddedServer.start();
}
@@ -47,8 +46,12 @@ public class LDAPRule extends ExternalResource {
return LDAP_CONNECTION_PROPERTIES_LOCATION;
}
- protected LDAPEmbeddedServer createServer(EmbeddedServersFactory factory) {
- return factory.createLdapServer();
+ protected LDAPEmbeddedServer createServer() {
+ Properties defaultProperties = new Properties();
+ defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY);
+ defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:ldap/users.ldif");
+
+ return new LDAPEmbeddedServer(defaultProperties);
}
public Map<String, String> getConfig() {
diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif
index 4d6d87e..176e19b 100644
--- a/testsuite/integration/src/test/resources/ldap/users.ldif
+++ b/testsuite/integration/src/test/resources/ldap/users.ldif
@@ -18,30 +18,3 @@ dn: ou=FinanceRoles,dc=keycloak,dc=org
objectclass: top
objectclass: organizationalUnit
ou: FinanceRoles
-
-dn: uid=jbrown,ou=People,dc=keycloak,dc=org
-objectclass: top
-objectclass: person
-objectclass: organizationalPerson
-objectclass: inetOrgPerson
-uid: jbrown
-cn: James
-sn: Brown
-mail: jbrown@keycloak.org
-postalCode: 88441
-userPassword: password
-
-dn: uid=bwilson,ou=People,dc=keycloak,dc=org
-objectclass: top
-objectclass: person
-objectclass: organizationalPerson
-objectclass: inetOrgPerson
-uid: bwilson
-cn: Bruce
-sn: Wilson
-sn: Schneider
-mail: bwilson@keycloak.org
-postalCode: 88441
-postalCode: 77332
-street: Elm 5
-userPassword: password
util/embedded-ldap/pom.xml 91(+91 -0)
diff --git a/util/embedded-ldap/pom.xml b/util/embedded-ldap/pom.xml
new file mode 100644
index 0000000..5a1e927
--- /dev/null
+++ b/util/embedded-ldap/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.4.0.Final-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-util-embedded-ldap</artifactId>
+ <name>Keycloak Util Embedded LDAP</name>
+ <description/>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-core-annotations</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.directory.jdbm</groupId>
+ <artifactId>apacheds-jdbm1</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-interceptor-kerberos</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-server-annotations</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.directory.jdbm</groupId>
+ <artifactId>apacheds-jdbm1</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-ldap-codec-standalone</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java
new file mode 100644
index 0000000..f0153ec
--- /dev/null
+++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java
@@ -0,0 +1,267 @@
+package org.keycloak.util.ldap;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.directory.api.ldap.model.constants.SchemaConstants;
+import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.schema.LdapComparator;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.api.ldap.model.schema.comparators.NormalizingComparator;
+import org.apache.directory.api.ldap.model.schema.registries.ComparatorRegistry;
+import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader;
+import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor;
+import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor;
+import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader;
+import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager;
+import org.apache.directory.api.util.exception.Exceptions;
+import org.apache.directory.server.constants.ServerDNConstants;
+import org.apache.directory.server.core.DefaultDirectoryService;
+import org.apache.directory.server.core.api.CacheService;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
+import org.apache.directory.server.core.api.partition.Partition;
+import org.apache.directory.server.core.api.schema.SchemaPartition;
+import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
+import org.apache.directory.server.core.factory.DirectoryServiceFactory;
+import org.apache.directory.server.core.factory.JdbmPartitionFactory;
+import org.apache.directory.server.core.factory.LdifPartitionFactory;
+import org.apache.directory.server.core.factory.PartitionFactory;
+import org.apache.directory.server.core.partition.ldif.LdifPartition;
+import org.apache.directory.server.i18n.I18n;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Slightly modified version of {@link DefaultDirectoryServiceFactory} which allows persistence among restarts and uses LDIF partitions by default
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class FileDirectoryServiceFactory implements DirectoryServiceFactory {
+
+ /** A logger for this class */
+ private static final Logger LOG = LoggerFactory.getLogger(FileDirectoryServiceFactory.class);
+
+ /** The directory service. */
+ private DirectoryService directoryService;
+
+ /** The partition factory. */
+ private PartitionFactory partitionFactory;
+
+
+ public FileDirectoryServiceFactory()
+ {
+ try
+ {
+ // creating the instance here so that
+ // we we can set some properties like accesscontrol, anon access
+ // before starting up the service
+ directoryService = new DefaultDirectoryService();
+
+ // no need to register a shutdown hook during tests because this
+ // starts a lot of threads and slows down test execution
+ directoryService.setShutdownHookEnabled( false );
+ }
+ catch ( Exception e )
+ {
+ throw new RuntimeException( e );
+ }
+
+ try
+ {
+ String typeName = System.getProperty( "apacheds.partition.factory" );
+
+ if ( typeName != null )
+ {
+ Class<? extends PartitionFactory> type = ( Class<? extends PartitionFactory> ) Class.forName( typeName );
+ partitionFactory = type.newInstance();
+ }
+ else
+ {
+ // partitionFactory = new JdbmPartitionFactory();
+ partitionFactory = new LdifPartitionFactory();
+ }
+ }
+ catch ( Exception e )
+ {
+ LOG.error( "Error instantiating custom partiton factory", e );
+ throw new RuntimeException( e );
+ }
+ }
+
+
+ public FileDirectoryServiceFactory( DirectoryService directoryService, PartitionFactory partitionFactory )
+ {
+ this.directoryService = directoryService;
+ this.partitionFactory = partitionFactory;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public void init( String name ) throws Exception
+ {
+ if ( ( directoryService != null ) && directoryService.isStarted() ) {
+ return;
+ }
+
+ build(name);
+ }
+
+
+ /**
+ * Build the working directory
+ */
+ private void buildInstanceDirectory( String name ) throws IOException
+ {
+ String instanceDirectory = System.getProperty( "workingDirectory" );
+
+ if ( instanceDirectory == null )
+ {
+ instanceDirectory = System.getProperty( "java.io.tmpdir" ) + "/server-work-" + name;
+ }
+
+ InstanceLayout instanceLayout = new InstanceLayout( instanceDirectory );
+
+ /*if ( instanceLayout.getInstanceDirectory().exists() )
+ {
+ try
+ {
+ FileUtils.deleteDirectory(instanceLayout.getInstanceDirectory());
+ }
+ catch ( IOException e )
+ {
+ LOG.warn( "couldn't delete the instance directory before initializing the DirectoryService", e );
+ }
+ }*/
+
+ directoryService.setInstanceLayout( instanceLayout );
+ }
+
+
+ /**
+ * Inits the schema and schema partition.
+ */
+ private void initSchema() throws Exception
+ {
+ File workingDirectory = directoryService.getInstanceLayout().getPartitionsDirectory();
+
+ // Extract the schema on disk (a brand new one) and load the registries
+ File schemaRepository = new File( workingDirectory, "schema" );
+ SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor( workingDirectory );
+
+ try
+ {
+ extractor.extractOrCopy();
+ }
+ catch ( IOException ioe )
+ {
+ // The schema has already been extracted, bypass
+ }
+
+ SchemaLoader loader = new LdifSchemaLoader( schemaRepository );
+ SchemaManager schemaManager = new DefaultSchemaManager( loader );
+
+ // We have to load the schema now, otherwise we won't be able
+ // to initialize the Partitions, as we won't be able to parse
+ // and normalize their suffix Dn
+ schemaManager.loadAllEnabled();
+
+ // Tell all the normalizer comparators that they should not normalize anything
+ ComparatorRegistry comparatorRegistry = schemaManager.getComparatorRegistry();
+
+ for ( LdapComparator<?> comparator : comparatorRegistry )
+ {
+ if ( comparator instanceof NormalizingComparator)
+ {
+ ( ( NormalizingComparator ) comparator ).setOnServer();
+ }
+ }
+
+ directoryService.setSchemaManager( schemaManager );
+
+ // Init the LdifPartition
+ LdifPartition ldifPartition = new LdifPartition( schemaManager, directoryService.getDnFactory() );
+ ldifPartition.setPartitionPath( new File( workingDirectory, "schema" ).toURI() );
+ SchemaPartition schemaPartition = new SchemaPartition( schemaManager );
+ schemaPartition.setWrappedPartition( ldifPartition );
+ directoryService.setSchemaPartition( schemaPartition );
+
+ List<Throwable> errors = schemaManager.getErrors();
+
+ if ( errors.size() != 0 )
+ {
+ throw new Exception( I18n.err(I18n.ERR_317, Exceptions.printErrors(errors)) );
+ }
+ }
+
+
+ /**
+ * Inits the system partition.
+ *
+ * @throws Exception the exception
+ */
+ private void initSystemPartition() throws Exception
+ {
+ // change the working directory to something that is unique
+ // on the system and somewhere either under target directory
+ // or somewhere in a temp area of the machine.
+
+ // Inject the System Partition
+ Partition systemPartition = partitionFactory.createPartition( directoryService.getSchemaManager(),
+ directoryService.getDnFactory(),
+ "system", ServerDNConstants.SYSTEM_DN, 500,
+ new File( directoryService.getInstanceLayout().getPartitionsDirectory(), "system" ) );
+ systemPartition.setSchemaManager(directoryService.getSchemaManager());
+
+ partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100 );
+
+ directoryService.setSystemPartition( systemPartition );
+ }
+
+
+ /**
+ * Builds the directory server instance.
+ *
+ * @param name the instance name
+ */
+ private void build( String name ) throws Exception
+ {
+ directoryService.setInstanceId( name );
+ buildInstanceDirectory( name );
+
+ CacheService cacheService = new CacheService();
+ cacheService.initialize( directoryService.getInstanceLayout() );
+
+ directoryService.setCacheService( cacheService );
+
+ // Init the service now
+ initSchema();
+ initSystemPartition();
+
+ directoryService.startup();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public DirectoryService getDirectoryService() throws Exception
+ {
+ return directoryService;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public PartitionFactory getPartitionFactory() throws Exception
+ {
+ return partitionFactory;
+ }
+}
diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java
new file mode 100644
index 0000000..18a1a30
--- /dev/null
+++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java
@@ -0,0 +1,272 @@
+package org.keycloak.util.ldap;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
+import org.apache.directory.api.ldap.model.ldif.LdifEntry;
+import org.apache.directory.api.ldap.model.ldif.LdifReader;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.partition.Partition;
+import org.apache.directory.server.core.factory.DirectoryServiceFactory;
+import org.apache.directory.server.core.factory.PartitionFactory;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.directory.server.protocol.shared.transport.Transport;
+import org.jboss.logging.Logger;
+import org.keycloak.util.FindFile;
+import org.keycloak.util.StreamUtil;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPEmbeddedServer {
+
+ private static final Logger log = Logger.getLogger(LDAPEmbeddedServer.class);
+
+ public static final String PROPERTY_BASE_DN = "ldap.baseDN";
+ public static final String PROPERTY_BIND_HOST = "ldap.host";
+ public static final String PROPERTY_BIND_PORT = "ldap.port";
+ public static final String PROPERTY_LDIF_FILE = "ldap.ldif";
+ public static final String PROPERTY_SASL_PRINCIPAL = "ldap.saslPrincipal";
+ public static final String PROPERTY_DSF = "ldap.dsf";
+
+ private static final String DEFAULT_BASE_DN = "dc=keycloak,dc=org";
+ private static final String DEFAULT_BIND_HOST = "localhost";
+ private static final String DEFAULT_BIND_PORT = "10389";
+ private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif";
+
+ public static final String DSF_INMEMORY = "mem";
+ public static final String DSF_FILE = "file";
+ public static final String DEFAULT_DSF = DSF_FILE;
+
+ protected Properties defaultProperties;
+
+ protected String baseDN;
+ protected String bindHost;
+ protected int bindPort;
+ protected String ldifFile;
+ protected String ldapSaslPrincipal;
+ protected String directoryServiceFactory;
+
+ protected DirectoryService directoryService;
+ protected LdapServer ldapServer;
+
+
+ public static void main(String[] args) throws Exception {
+ Properties defaultProperties = new Properties();
+ defaultProperties.put(PROPERTY_DSF, DSF_FILE);
+
+ execute(args, defaultProperties);
+ }
+
+ public static void execute(String[] args, Properties defaultProperties) throws Exception {
+ final LDAPEmbeddedServer ldapEmbeddedServer = new LDAPEmbeddedServer(defaultProperties);
+ ldapEmbeddedServer.init();
+ ldapEmbeddedServer.start();
+
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+
+ @Override
+ public void run() {
+ try {
+ ldapEmbeddedServer.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ });
+ }
+
+ public LDAPEmbeddedServer(Properties defaultProperties) {
+ this.defaultProperties = defaultProperties;
+
+ this.baseDN = readProperty(PROPERTY_BASE_DN, DEFAULT_BASE_DN);
+ this.bindHost = readProperty(PROPERTY_BIND_HOST, DEFAULT_BIND_HOST);
+ String bindPort = readProperty(PROPERTY_BIND_PORT, DEFAULT_BIND_PORT);
+ this.bindPort = Integer.parseInt(bindPort);
+ this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE);
+ this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null);
+ this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF);
+ }
+
+ protected String readProperty(String propertyName, String defaultValue) {
+ String value = System.getProperty(propertyName);
+
+ if (value == null || value.isEmpty()) {
+ value = (String) this.defaultProperties.get(propertyName);
+ }
+
+ if (value == null || value.isEmpty()) {
+ value = defaultValue;
+ }
+
+ return value;
+ }
+
+
+ public void init() throws Exception {
+ log.info("Creating LDAP Directory Service. Config: baseDN=" + baseDN + ", bindHost=" + bindHost + ", bindPort=" + bindPort +
+ ", ldapSaslPrincipal=" + ldapSaslPrincipal + ", directoryServiceFactory=" + directoryServiceFactory + ", ldif=" + ldifFile);
+
+ this.directoryService = createDirectoryService();
+
+ log.info("Importing LDIF: " + ldifFile);
+ importLdif();
+
+ log.info("Creating LDAP Server");
+ this.ldapServer = createLdapServer();
+ }
+
+
+ public void start() throws Exception {
+ log.info("Starting LDAP Server");
+ ldapServer.start();
+ log.info("LDAP Server started");
+ }
+
+
+ protected DirectoryService createDirectoryService() throws Exception {
+ // Parse "keycloak" from "dc=keycloak,dc=org"
+ String dcName = baseDN.split(",")[0];
+ dcName = dcName.substring(dcName.indexOf("=") + 1);
+
+ DirectoryServiceFactory dsf;
+ if (this.directoryServiceFactory.equals(DSF_INMEMORY)) {
+ dsf = new InMemoryDirectoryServiceFactory();
+ } else if (this.directoryServiceFactory.equals(DSF_FILE)) {
+ dsf = new FileDirectoryServiceFactory();
+ } else {
+ throw new IllegalStateException("Unknown value of directoryServiceFactory: " + this.directoryServiceFactory);
+ }
+
+ DirectoryService service = dsf.getDirectoryService();
+ service.setAccessControlEnabled(false);
+ service.setAllowAnonymousAccess(false);
+ service.getChangeLog().setEnabled(false);
+
+ dsf.init(dcName + "DS");
+
+ SchemaManager schemaManager = service.getSchemaManager();
+
+ PartitionFactory partitionFactory = dsf.getPartitionFactory();
+ Partition partition = partitionFactory.createPartition(
+ schemaManager,
+ service.getDnFactory(),
+ dcName,
+ this.baseDN,
+ 1000,
+ new File(service.getInstanceLayout().getPartitionsDirectory(), dcName));
+ partition.setCacheService( service.getCacheService() );
+ partition.initialize();
+
+ partition.setSchemaManager( schemaManager );
+
+ // Inject the partition into the DirectoryService
+ service.addPartition( partition );
+
+ // Last, process the context entry
+ String entryLdif =
+ "dn: " + baseDN + "\n" +
+ "dc: " + dcName + "\n" +
+ "objectClass: top\n" +
+ "objectClass: domain\n\n";
+ importLdifContent(service, entryLdif);
+
+ return service;
+ }
+
+
+ protected LdapServer createLdapServer() {
+ LdapServer ldapServer = new LdapServer();
+
+ ldapServer.setServiceName("DefaultLdapServer");
+ ldapServer.setSearchBaseDn(this.baseDN);
+
+ // Read the transports
+ Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50);
+ ldapServer.addTransports( ldap );
+
+ // Associate the DS to this LdapServer
+ ldapServer.setDirectoryService( directoryService );
+
+ // Propagate the anonymous flag to the DS
+ directoryService.setAllowAnonymousAccess(false);
+
+ return ldapServer;
+ }
+
+
+ private void importLdif() throws Exception {
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("hostname", this.bindHost);
+ if (this.ldapSaslPrincipal != null) {
+ map.put("ldapSaslPrincipal", this.ldapSaslPrincipal);
+ }
+
+ // Find LDIF file on filesystem or classpath ( if it's like classpath:ldap/users.ldif )
+ InputStream is = FindFile.findFile(ldifFile);
+ if (is == null) {
+ throw new IllegalStateException("LDIF file not found on classpath or on file system. Location was: " + ldifFile);
+ }
+
+ final String ldifContent = StrSubstitutor.replace(StreamUtil.readString(is), map);
+ log.info("Content of LDIF: " + ldifContent);
+ final SchemaManager schemaManager = directoryService.getSchemaManager();
+
+ importLdifContent(directoryService, ldifContent);
+ }
+
+ private static void importLdifContent(DirectoryService directoryService, String ldifContent) throws Exception {
+ LdifReader ldifReader = new LdifReader(IOUtils.toInputStream(ldifContent));
+
+ try {
+ for (LdifEntry ldifEntry : ldifReader) {
+ try {
+ directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry()));
+ } catch (LdapEntryAlreadyExistsException ignore) {
+ log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring");
+ }
+ }
+ } finally {
+ ldifReader.close();
+ }
+ }
+
+
+ public void stop() throws Exception {
+ stopLdapServer();
+ shutdownDirectoryService();
+ }
+
+
+ protected void stopLdapServer() {
+ log.info("Stopping LDAP server.");
+ ldapServer.stop();
+ }
+
+
+ protected void shutdownDirectoryService() throws Exception {
+ log.info("Stopping Directory service.");
+ directoryService.shutdown();
+
+ // Delete workfiles just for 'inmemory' implementation used in tests. Normally we want LDAP data to persist
+ File instanceDir = directoryService.getInstanceLayout().getInstanceDirectory();
+ if (this.directoryServiceFactory.equals(DSF_INMEMORY)) {
+ log.infof("Removing Directory service workfiles: %s", instanceDir.getAbsolutePath());
+ FileUtils.deleteDirectory(instanceDir);
+ } else {
+ log.info("Working LDAP directory not deleted. Delete it manually if you want to start with fresh LDAP data. Directory location: " + instanceDir.getAbsolutePath());
+ }
+ }
+
+}
diff --git a/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif
new file mode 100644
index 0000000..fd9936c
--- /dev/null
+++ b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif
@@ -0,0 +1,90 @@
+dn: dc=keycloak,dc=org
+objectclass: dcObject
+objectclass: organization
+o: Keycloak
+dc: Keycloak
+
+dn: ou=People,dc=keycloak,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: People
+
+dn: uid=krbtgt,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: KDC Service
+sn: Service
+uid: krbtgt
+userPassword: secret
+krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=ldap,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: LDAP
+sn: Service
+uid: ldap
+userPassword: randall
+krb5PrincipalName: ${ldapSaslPrincipal}
+krb5KeyVersionNumber: 0
+
+dn: uid=HTTP,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: HTTP
+sn: Service
+uid: HTTP
+userPassword: httppwd
+krb5PrincipalName: HTTP/${hostname}@KEYCLOAK.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=hnelson,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: Horatio
+sn: Nelson
+mail: hnelson@keycloak.org
+uid: hnelson
+userPassword: secret
+krb5PrincipalName: hnelson@KEYCLOAK.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=jduke,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: Java
+sn: Duke
+mail: jduke@keycloak.org
+uid: jduke
+userPassword: theduke
+krb5PrincipalName: jduke@KEYCLOAK.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=gsstestserver,ou=People,dc=keycloak,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: gsstestserver
+sn: Service
+uid: gsstestserver
+userPassword: gsstestpwd
+krb5PrincipalName: gsstestserver/xxx@KEYCLOAK.ORG
+krb5KeyVersionNumber: 0
diff --git a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
new file mode 100644
index 0000000..4d6d87e
--- /dev/null
+++ b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
@@ -0,0 +1,47 @@
+dn: dc=keycloak,dc=org
+objectclass: dcObject
+objectclass: organization
+o: Keycloak
+dc: Keycloak
+
+dn: ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: organizationalUnit
+ou: People
+
+dn: ou=RealmRoles,dc=keycloak,dc=org
+objectclass: top
+objectclass: organizationalUnit
+ou: RealmRoles
+
+dn: ou=FinanceRoles,dc=keycloak,dc=org
+objectclass: top
+objectclass: organizationalUnit
+ou: FinanceRoles
+
+dn: uid=jbrown,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: jbrown
+cn: James
+sn: Brown
+mail: jbrown@keycloak.org
+postalCode: 88441
+userPassword: password
+
+dn: uid=bwilson,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: bwilson
+cn: Bruce
+sn: Wilson
+sn: Schneider
+mail: bwilson@keycloak.org
+postalCode: 88441
+postalCode: 77332
+street: Elm 5
+userPassword: password
diff --git a/util/embedded-ldap/src/main/resources/log4j.properties b/util/embedded-ldap/src/main/resources/log4j.properties
new file mode 100644
index 0000000..37d89b1
--- /dev/null
+++ b/util/embedded-ldap/src/main/resources/log4j.properties
@@ -0,0 +1,9 @@
+log4j.rootLogger=info, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
+
+log4j.logger.org.keycloak=info
+log4j.logger.org.apache.directory.api=warn
+log4j.logger.org.apache.directory.server.core=warn
\ No newline at end of file
util/pom.xml 22(+22 -0)
diff --git a/util/pom.xml b/util/pom.xml
new file mode 100644
index 0000000..3026307
--- /dev/null
+++ b/util/pom.xml
@@ -0,0 +1,22 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.4.0.Final-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <name>Keycloak Util Parent</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-util-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>embedded-ldap</module>
+ </modules>
+
+</project>
\ No newline at end of file