Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2015-07-06 17:44:52 -04:00
commit d49c7d47a2
48 changed files with 1607 additions and 584 deletions

View file

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

View file

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

View file

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

View file

@ -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);

View file

@ -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>

View file

@ -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
```

View file

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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>

View file

20
examples/ldap/pom.xml Normal file
View file

@ -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>

View file

@ -44,5 +44,6 @@
<module>kerberos</module>
<module>themes</module>
<module>saml</module>
<module>ldap</module>
</modules>
</project>

View file

@ -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
*

View file

@ -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();
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);
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);
}
}
});
}
}
}
return syncResult;
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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);

View file

@ -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

11
pom.xml
View file

@ -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 -->
@ -1120,6 +1117,11 @@
<version>${project.version}</version>
<type>zip</type>
</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>
@ -1184,6 +1186,7 @@
<version>${project.version}</version>
<classifier>classes</classifier>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>federation-properties-example</artifactId>

View file

@ -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;

View file

@ -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

View file

@ -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>

View file

@ -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));

View file

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

View file

@ -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");

View file

@ -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

View file

@ -1,4 +1,4 @@
package org.keycloak.testsuite.ldap;
package org.keycloak.testsuite.federation;
import java.io.File;
import java.io.InputStream;

View file

@ -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);
}
@ -154,6 +154,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();
@ -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);
}
}

View file

@ -1,110 +0,0 @@
package org.keycloak.testsuite.ldap;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.keycloak.util.KerberosSerializationUtils;
import sun.security.jgss.GSSNameImpl;
import sun.security.jgss.krb5.Krb5NameElement;
/**
* Factory for ApacheDS based LDAP and Kerberos servers
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class EmbeddedServersFactory {
private static final String DEFAULT_BASE_DN = "dc=keycloak,dc=org";
private static final String DEFAULT_BIND_HOST = "localhost";
private static final int DEFAULT_BIND_PORT = 10389;
private static final String DEFAULT_LDIF_FILE = "ldap/users.ldif";
private static final String DEFAULT_KERBEROS_LDIF_FILE = "kerberos/users-kerberos.ldif";
private static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG";
private static final int DEFAULT_KDC_PORT = 6088;
private static final String DEFAULT_KDC_ENCRYPTION_TYPES = "aes128-cts-hmac-sha1-96, des-cbc-md5, des3-cbc-sha1-kd";
private String baseDN;
private String bindHost;
private int bindPort;
private String ldapSaslPrincipal;
private String ldifFile;
private String kerberosRealm;
private int kdcPort;
private String kdcEncryptionTypes;
public static EmbeddedServersFactory readConfiguration() {
EmbeddedServersFactory factory = new EmbeddedServersFactory();
factory.readProperties();
return factory;
}
protected void readProperties() {
this.baseDN = System.getProperty("ldap.baseDN");
this.bindHost = System.getProperty("ldap.host");
String bindPort = System.getProperty("ldap.port");
this.ldifFile = System.getProperty("ldap.ldif");
this.ldapSaslPrincipal = System.getProperty("ldap.saslPrincipal");
this.kerberosRealm = System.getProperty("kerberos.realm");
String kdcPort = System.getProperty("kerberos.port");
this.kdcEncryptionTypes = System.getProperty("kerberos.encTypes");
if (baseDN == null || baseDN.isEmpty()) {
baseDN = DEFAULT_BASE_DN;
}
if (bindHost == null || bindHost.isEmpty()) {
bindHost = DEFAULT_BIND_HOST;
}
this.bindPort = (bindPort == null || bindPort.isEmpty()) ? DEFAULT_BIND_PORT : Integer.parseInt(bindPort);
if (ldifFile == null || ldifFile.isEmpty()) {
ldifFile = DEFAULT_LDIF_FILE;
}
if (kerberosRealm == null || kerberosRealm.isEmpty()) {
kerberosRealm = DEFAULT_KERBEROS_REALM;
}
this.kdcPort = (kdcPort == null || kdcPort.isEmpty()) ? DEFAULT_KDC_PORT : Integer.parseInt(kdcPort);
if (kdcEncryptionTypes == null || kdcEncryptionTypes.isEmpty()) {
kdcEncryptionTypes = DEFAULT_KDC_ENCRYPTION_TYPES;
}
}
public LDAPEmbeddedServer createLdapServer() {
// Override LDIF file with default for embedded LDAP
if (ldifFile.equals(DEFAULT_KERBEROS_LDIF_FILE)) {
ldifFile = DEFAULT_LDIF_FILE;
}
return new LDAPEmbeddedServer(baseDN, bindHost, bindPort, ldifFile, ldapSaslPrincipal);
}
public KerberosEmbeddedServer createKerberosServer() {
// Override LDIF file with default for embedded Kerberos
if (ldifFile.equals(DEFAULT_LDIF_FILE)) {
ldifFile = DEFAULT_KERBEROS_LDIF_FILE;
}
// Init ldap sasl principal just when creating kerberos server
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
try {
// Same algorithm like sun.security.krb5.PrincipalName constructor
GSSName gssName = GSSManager.getInstance().createName("ldap@" + bindHost, GSSName.NT_HOSTBASED_SERVICE);
GSSNameImpl gssName1 = (GSSNameImpl) gssName;
Krb5NameElement krb5NameElement = (Krb5NameElement) gssName1.getElement(KerberosSerializationUtils.KRB5_OID);
this.ldapSaslPrincipal = krb5NameElement.getKrb5PrincipalName().toString();
} catch (GSSException uhe) {
throw new RuntimeException(uhe);
}
}
return new KerberosEmbeddedServer(baseDN, bindHost, bindPort, ldifFile, ldapSaslPrincipal, kerberosRealm, kdcPort, kdcEncryptionTypes);
}
}

View file

@ -1,196 +0,0 @@
package org.keycloak.testsuite.ldap;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
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.DSAnnotationProcessor;
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.StreamUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPEmbeddedServer {
private static final Logger log = Logger.getLogger(LDAPEmbeddedServer.class);
protected final String baseDN;
protected final String bindHost;
protected final int bindPort;
protected final String ldifFile;
protected final String ldapSaslPrincipal;
protected DirectoryService directoryService;
protected LdapServer ldapServer;
public static void main(String[] args) throws Exception {
EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration();
LDAPEmbeddedServer ldapEmbeddedServer = factory.createLdapServer();
ldapEmbeddedServer.init();
ldapEmbeddedServer.start();
}
public LDAPEmbeddedServer(String baseDN, String bindHost, int bindPort, String ldifFile, String ldapSaslPrincipal) {
this.baseDN = baseDN;
this.bindHost = bindHost;
this.bindPort = bindPort;
this.ldifFile = ldifFile;
this.ldapSaslPrincipal = ldapSaslPrincipal;
}
public void init() throws Exception {
log.info("Creating LDAP Directory Service. Config: baseDN=" + baseDN + ", bindHost=" + bindHost + ", bindPort=" + bindPort +
", ldapSaslPrincipal=" + ldapSaslPrincipal);
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].substring(3);
InMemoryDirectoryServiceFactory dsf = new InMemoryDirectoryServiceFactory();
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";
DSAnnotationProcessor.injectEntries(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);
}
// For now, assume that LDIF file is on classpath
InputStream is;
if (ldifFile.startsWith("file:")) {
is = new URL(ldifFile).openStream();
} else {
is = getClass().getClassLoader().getResourceAsStream(ldifFile);
}
if (is == null) {
throw new IllegalStateException("LDIF file not found on classpath. Location was: " + ldifFile);
}
final String ldifContent = StrSubstitutor.replace(StreamUtil.readString(is), map);
log.info("Content of LDIF: " + ldifContent);
final SchemaManager schemaManager = directoryService.getSchemaManager();
for (LdifEntry ldifEntry : new LdifReader(IOUtils.toInputStream(ldifContent))) {
try {
directoryService.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry()));
} catch (LdapEntryAlreadyExistsException ignore) {
log.debug("Entry " + ldifEntry.getNewRdn() + " already exists. Ignoring");
}
}
}
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();
log.info("Removing Directory service workfiles.");
FileUtils.deleteDirectory(directoryService.getInstanceLayout().getInstanceDirectory());
}
}

View file

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

View file

@ -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() {

View file

@ -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

View file

@ -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>

View file

@ -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;
}
}

View file

@ -1,4 +1,4 @@
package org.keycloak.testsuite.ldap;
package org.keycloak.util.ldap;
import java.io.File;
import java.io.IOException;
@ -76,7 +76,7 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
directoryService.setInstanceId(name);
// instance layout
InstanceLayout instanceLayout = new InstanceLayout(System.getProperty("java.io.tmpdir") + "/server-work-" + name);
InstanceLayout instanceLayout = new InstanceLayout(System.getProperty("java.io.tmpdir") + "/server-work-inmemory-" + name);
if (instanceLayout.getInstanceDirectory().exists()) {
try {
FileUtils.deleteDirectory(instanceLayout.getInstanceDirectory());

View file

@ -1,4 +1,4 @@
package org.keycloak.testsuite.ldap;
package org.keycloak.util.ldap;
import java.net.URL;
import java.util.Map;
@ -61,7 +61,7 @@ class InMemorySchemaPartition extends AbstractLdifPartition {
// add mandatory attributes
if (entry.get(SchemaConstants.ENTRY_CSN_AT) == null) {
entry.add(SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString());
entry.add(SchemaConstants.ENTRY_CSN_AT, AbstractLdifPartition.defaultCSNFactory.newInstance().toString());
}
if (entry.get(SchemaConstants.ENTRY_UUID_AT) == null) {
entry.add(SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString());

View file

@ -1,9 +1,10 @@
package org.keycloak.testsuite.ldap;
package org.keycloak.util.ldap;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import javax.security.auth.kerberos.KerberosPrincipal;
@ -25,7 +26,13 @@ import org.apache.directory.server.protocol.shared.transport.UdpTransport;
import org.apache.directory.shared.kerberos.KerberosTime;
import org.apache.directory.shared.kerberos.KerberosUtils;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.jboss.logging.Logger;
import org.keycloak.util.KerberosSerializationUtils;
import sun.security.jgss.GSSNameImpl;
import sun.security.jgss.krb5.Krb5NameElement;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -34,6 +41,16 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
private static final Logger log = Logger.getLogger(KerberosEmbeddedServer.class);
public static final String PROPERTY_KERBEROS_REALM = "kerberos.realm";
public static final String PROPERTY_KDC_PORT = "kerberos.port";
public static final String PROPERTY_KDC_ENCTYPES = "kerberos.encTypes";
private static final String DEFAULT_KERBEROS_LDIF_FILE = "classpath:kerberos/default-users.ldif";
private static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG";
private static final String DEFAULT_KDC_PORT = "6088";
private static final String DEFAULT_KDC_ENCRYPTION_TYPES = "aes128-cts-hmac-sha1-96, des-cbc-md5, des3-cbc-sha1-kd";
private final String kerberosRealm;
private final int kdcPort;
private final String kdcEncryptionTypes;
@ -42,18 +59,53 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
public static void main(String[] args) throws Exception {
EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration();
KerberosEmbeddedServer kerberosEmbeddedServer = factory.createKerberosServer();
Properties defaultProperties = new Properties();
defaultProperties.put(PROPERTY_DSF, DSF_FILE);
execute(args, defaultProperties);
}
public static void execute(String[] args, Properties defaultProperties) throws Exception {
final KerberosEmbeddedServer kerberosEmbeddedServer = new KerberosEmbeddedServer(defaultProperties);
kerberosEmbeddedServer.init();
kerberosEmbeddedServer.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
kerberosEmbeddedServer.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
protected KerberosEmbeddedServer(String baseDN, String bindHost, int bindPort, String ldifFile, String ldapSaslPrincipal, String kerberosRealm, int kdcPort, String kdcEncryptionTypes) {
super(baseDN, bindHost, bindPort, ldifFile, ldapSaslPrincipal);
this.kdcEncryptionTypes = kdcEncryptionTypes;
this.kerberosRealm = kerberosRealm;
this.kdcPort = kdcPort;
public KerberosEmbeddedServer(Properties defaultProperties) {
super(defaultProperties);
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_KERBEROS_LDIF_FILE);
this.kerberosRealm = readProperty(PROPERTY_KERBEROS_REALM, DEFAULT_KERBEROS_REALM);
String kdcPort = readProperty(PROPERTY_KDC_PORT, DEFAULT_KDC_PORT);
this.kdcPort = Integer.parseInt(kdcPort);
this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES);
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
try {
// Same algorithm like sun.security.krb5.PrincipalName constructor
GSSName gssName = GSSManager.getInstance().createName("ldap@" + bindHost, GSSName.NT_HOSTBASED_SERVICE);
GSSNameImpl gssName1 = (GSSNameImpl) gssName;
Krb5NameElement krb5NameElement = (Krb5NameElement) gssName1.getElement(KerberosSerializationUtils.KRB5_OID);
this.ldapSaslPrincipal = krb5NameElement.getKrb5PrincipalName().toString();
} catch (GSSException uhe) {
throw new RuntimeException(uhe);
}
}
}
@ -79,7 +131,7 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
protected LdapServer createLdapServer() {
LdapServer ldapServer = super.createLdapServer();
ldapServer.setSaslHost( this.bindHost );
ldapServer.setSaslHost(this.bindHost);
ldapServer.setSaslPrincipal( this.ldapSaslPrincipal);
ldapServer.setSaslRealms(new ArrayList<String>());

View file

@ -1,10 +1,11 @@
package org.keycloak.testsuite.ldap;
package org.keycloak.util.ldap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
import org.apache.directory.server.kerberos.shared.keytab.Keytab;
@ -34,7 +35,7 @@ public class KerberosKeytabCreator {
System.out.println("-------------------------");
System.out.println("Arguments missing or invalid. Required arguments are: <principalName> <passPhrase> <outputKeytabFile>");
System.out.println("Example of usage:");
System.out.println("mvn exec:java -Dexec.mainClass=\"org.keycloak.testsuite.ldap.KerberosKeytabCreator\" -Dexec.args=\"HTTP/localhost@KEYCLOAK.ORG httppwd src/main/resources/kerberos/http.keytab\"");
System.out.println("java -jar embedded-ldap/target/embedded-ldap.jar keytabCreator HTTP/localhost@KEYCLOAK.ORG httppassword /tmp/http.keytab");
} else {
final File keytabFile = new File(args[2]);
createKeytab(args[0], args[1], keytabFile);
@ -42,6 +43,11 @@ public class KerberosKeytabCreator {
}
}
// Just for the reflection purposes
public static void execute(String[] args, Properties defaultProperties) throws Exception {
main(args);
}
/**
* Creates a keytab file for given principal.
*

View file

@ -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());
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

22
util/pom.xml Normal file
View file

@ -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>