diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java index e592df9573..b93e2b95de 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java @@ -89,13 +89,6 @@ public class BasicDBObjectMapper implements Mapper { Type[] genericTypeArguments = parameterized.getActualTypeArguments(); List 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(valueFromDB, expectedReturnType, genericTypes); diff --git a/examples/kerberos/README.md b/examples/kerberos/README.md index 80e1ff8eb5..5993a1f473 100644 --- a/examples/kerberos/README.md +++ b/examples/kerberos/README.md @@ -19,7 +19,7 @@ cp http.keytab /tmp/http.keytab ``` Alternative is to configure different location for `keyTab` property in `kerberosrealm.json` configuration file (On Windows this will be needed). -Note that in production, keytab file should be in secured location accessible just to the user under which is Keycloak server running. +WARNING: In production, keytab file should be in secured location accessible just to the user under which is Keycloak server running. **3)** Run Keycloak server and import `kerberosrealm.json` into it through admin console. This will import realm with sample application @@ -37,12 +37,13 @@ Also if you are on Linux, make sure that record like: ``` is in your `/etc/hosts` before other records for the 127.0.0.1 host to avoid issues related to incompatible reverse lookup (Ensure the similar for other OS as well) +**4)** Install kerberos client. This is platform dependent. If you are on Fedora, Ubuntu or RHEL, you can install package `freeipa-client`, which contains Kerberos client and bunch of other stuff. -**4)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm and enable `forwardable` flag, which is needed +**5)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm for host `localhost` and enable `forwardable` flag, which is needed for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server. See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/test/resources/kerberos/test-krb5.conf) for inspiration. -**5)** Run ApacheDS based Kerberos server embedded in Keycloak. Easiest is to checkout keycloak sources, build and then run KerberosEmbeddedServer +**6)** Run ApacheDS based Kerberos server embedded in Keycloak. Easiest is to checkout keycloak sources, build and then run KerberosEmbeddedServer as shown here: ``` @@ -55,12 +56,12 @@ mvn exec:java -Pkerberos More details about embedded Kerberos server in [testsuite README](https://github.com/keycloak/keycloak/blob/master/misc/Testsuite.md#kerberos-server). -**6)** Configure browser (Firefox, Chrome or other) and enable SPNEGO authentication and credential delegation for `localhost` . +**7)** Configure browser (Firefox, Chrome or other) and enable SPNEGO authentication and credential delegation for `localhost` . In Firefox it can be done by adding `localhost` to both `network.negotiate-auth.trusted-uris` and `network.negotiate-auth.delegation-uris` . More info in [testsuite README](https://github.com/keycloak/keycloak/blob/master/misc/Testsuite.md#kerberos-server). -**7)** Test the example. Obtain kerberos ticket by running command from CMD (on linux): +**8)** Test the example. Obtain kerberos ticket by running command from CMD (on linux): ``` kinit hnelson@KEYCLOAK.ORG ``` diff --git a/examples/kerberos/users.ldif b/examples/kerberos/users.ldif new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/ldap/embedded-ldap/assembly.xml b/examples/ldap/embedded-ldap/assembly.xml new file mode 100644 index 0000000000..58afeca05b --- /dev/null +++ b/examples/ldap/embedded-ldap/assembly.xml @@ -0,0 +1,21 @@ + + embedded-ldap + + + dir + + + false + + + + false + true + true + + org.keycloak:keycloak-util-embedded-ldap + org.slf4j:slf4j-log4j12 + + + + \ No newline at end of file diff --git a/examples/ldap/embedded-ldap/pom.xml b/examples/ldap/embedded-ldap/pom.xml new file mode 100644 index 0000000000..b981072adb --- /dev/null +++ b/examples/ldap/embedded-ldap/pom.xml @@ -0,0 +1,82 @@ + + + + keycloak-examples-ldap-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + + + 4.0.0 + org.keycloak.example.demo + keycloak-examples-embedded-ldap + jar + LDAP Demo Application + + + + org.keycloak + keycloak-util-embedded-ldap + + + org.slf4j + slf4j-log4j12 + compile + + + + + embedded-ldap + + + maven-assembly-plugin + + + assemble + package + + single + + + + assembly.xml + + + target + + + target/assembly/work + + false + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.keycloak.example.ldap.embedded.EmbeddedLDAPLauncher + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + \ No newline at end of file diff --git a/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java b/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java new file mode 100644 index 0000000000..b3941d5a4e --- /dev/null +++ b/examples/ldap/embedded-ldap/src/main/java/org/keycloak/example/ldap/embedded/EmbeddedLDAPLauncher.java @@ -0,0 +1,127 @@ +package org.keycloak.example.ldap.embedded; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This is supposed to be executed from JAR file (java -jar target/embedded-ldap.jar ). For executing from IDE or Maven use directly + * the proper class (LDAPEmbeddedServer, KerberosEmbeddedServer or KerberosKeytabCreator) + * + * @author Marek Posolda + */ +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 jars = new ArrayList(); + 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; + } +} diff --git a/examples/ldap/ldap-app/pom.xml b/examples/ldap/ldap-app/pom.xml new file mode 100644 index 0000000000..d080ba6a05 --- /dev/null +++ b/examples/ldap/ldap-app/pom.xml @@ -0,0 +1,68 @@ + + + + keycloak-examples-ldap-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + + + 4.0.0 + org.keycloak.example.demo + keycloak-examples-ldap-app + war + LDAP Demo Application + + + + jboss + jboss repo + http://repository.jboss.org/nexus/content/groups/public/ + + + + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-adapter-core + provided + + + org.apache.httpcomponents + httpclient + provided + + + + + ldap-portal + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + + \ No newline at end of file diff --git a/examples/ldap/ldap-app/users.ldif b/examples/ldap/ldap-app/users.ldif new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/ldap/pom.xml b/examples/ldap/pom.xml new file mode 100644 index 0000000000..d506efc1cc --- /dev/null +++ b/examples/ldap/pom.xml @@ -0,0 +1,20 @@ + + + keycloak-examples-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + + Keycloak LDAP Examples - Parent + + 4.0.0 + + keycloak-examples-ldap-parent + pom + + + embedded-ldap + ldap-app + + + \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index b2f7f2ad81..4815c397d5 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -44,5 +44,6 @@ kerberos themes saml + ldap diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 66665bf8f7..b5598e935b 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -13,7 +13,9 @@ import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -26,6 +28,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; import org.keycloak.constants.KerberosConstants; +import org.keycloak.models.utils.KeycloakModelUtils; import java.util.ArrayList; import java.util.Arrays; @@ -176,7 +179,7 @@ public class LDAPFederationProvider implements UserFederationProvider { for (LDAPObject ldapUser : ldapUsers) { String ldapUsername = LDAPUtils.getUsername(ldapUser, this.ldapIdentityStore.getConfig()); if (session.userStorage().getUserByUsername(ldapUsername, realm) == null) { - UserModel imported = importUserFromLDAP(realm, ldapUser); + UserModel imported = importUserFromLDAP(session, realm, ldapUser); searchResults.add(imported); } } @@ -249,10 +252,10 @@ public class LDAPFederationProvider implements UserFederationProvider { return null; } - return importUserFromLDAP(realm, ldapUser); + return importUserFromLDAP(session, realm, ldapUser); } - protected UserModel importUserFromLDAP(RealmModel realm, LDAPObject ldapUser) { + protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) { String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig()); if (ldapUsername == null) { @@ -298,7 +301,7 @@ public class LDAPFederationProvider implements UserFederationProvider { return null; } - return importUserFromLDAP(realm, ldapUser); + return importUserFromLDAP(session, realm, ldapUser); } @Override @@ -383,38 +386,6 @@ public class LDAPFederationProvider implements UserFederationProvider { public void close() { } - protected UserFederationSyncResult importLDAPUsers(RealmModel realm, List 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 federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId()); - for (UserFederationMapperModel mapperModel : federationMappers) { - LDAPFederationMapper ldapMapper = getMapper(mapperModel); - ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, currentUser, realm, false); - } - - logger.debugf("Updated user from LDAP: %s", currentUser.getUsername()); - syncResult.increaseUpdated(); - } else { - logger.warnf("User '%s' is not updated during sync as he is not linked to federation provider '%s'", username, fedModel.getDisplayName()); - } - } - } - - return syncResult; - } - /** * Called after successful kerberos authentication * diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 2b60ff83b9..a71b84d1fd 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -14,12 +14,15 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory; +import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationEventAwareProviderFactory; import org.keycloak.models.UserFederationMapperModel; @@ -94,7 +97,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false"); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false", + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); // CN is typically used as RDN for Active Directory deployments @@ -107,7 +111,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); } else { @@ -118,14 +123,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username-cn", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false"); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false", + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); } else { @@ -141,7 +148,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); } @@ -149,14 +157,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "true"); realm.addUserFederationMapper(mapperModel); mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly, - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false", + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); realm.addUserFederationMapper(mapperModel); String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP; @@ -167,7 +177,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName, UserAttributeLDAPFederationMapper.READ_ONLY, "true", - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); realm.addUserFederationMapper(mapperModel); // map modifyTimeStamp as read-only @@ -175,7 +186,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName, UserAttributeLDAPFederationMapper.READ_ONLY, "true", - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); realm.addUserFederationMapper(mapperModel); } @@ -226,29 +238,14 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi userQuery.setLimit(pageSize); final List 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 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 ldapUsers) { + final UserFederationSyncResult syncResult = new UserFederationSyncResult(); - protected UserFederationSyncResult importLdapUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List 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 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) { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java index 67e92c7e2c..141ae387d7 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -3,6 +3,7 @@ package org.keycloak.federation.ldap.idm.store.ldap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; @@ -437,18 +438,26 @@ public class LDAPIdentityStore implements IdentityStore { // ldapObject.getReadOnlyAttributeNames() are lower-cased if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) { - BasicAttribute attr = new BasicAttribute(attrName); + if (attrValue == null) { - // Adding empty value as we don't know if attribute is mandatory in LDAP - attr.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE); - } else { - for (String val : attrValue) { - if (val == null || val.toString().trim().length() == 0) { - val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE; - } - attr.add(val); - } + // Shouldn't happen + logger.warnf("Attribute '%s' is null on LDAP object '%s' . Using empty value to be saved to LDAP", attrName, ldapObject.getDn().toString()); + attrValue = Collections.emptySet(); } + + // Ignore empty attributes during create + if (isCreate && attrValue.isEmpty()) { + continue; + } + + BasicAttribute attr = new BasicAttribute(attrName); + for (String val : attrValue) { + if (val == null || val.toString().trim().length() == 0) { + val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE; + } + attr.add(val); + } + entryAttributes.put(attr); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index 4565f885ca..e29ab6f9a0 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -239,7 +239,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { Set memberships = getExistingMemberships(mapperModel, ldapRole); memberships.remove(ldapUser.getDn().toString()); - // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But on active directory! (Empty membership is not allowed here) + // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Empty membership is not allowed here) if (memberships.size() == 0 && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) { memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java index 24bf4509e5..283089a0ba 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java @@ -3,6 +3,7 @@ package org.keycloak.federation.ldap.mappers; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -15,6 +16,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.QueryParameter; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; +import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; @@ -58,6 +60,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public static final String LDAP_ATTRIBUTE = "ldap.attribute"; public static final String READ_ONLY = "read.only"; public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap"; + public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap"; @Override @@ -88,6 +91,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); + boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP); Property 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()); + } + } else { + ldapUser.setSingleAttribute(ldapAttrName, attrValue.toString()); + } } else { // we don't have java property. Let's set attribute List 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()); + } } 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()); + } } else if (value instanceof String) { ldapUser.setSingleAttribute(ldapAttrName, (String) value); } else { List asList = (List) 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 ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName); if (ldapAttrValue == null) { - return null; + return Collections.emptyList(); } else { return new ArrayList<>(ldapAttrValue); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java index 1b1b44d2bd..c14d0e89b3 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java @@ -31,9 +31,13 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false"); configProperties.add(readOnly); - ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always read value from LDAP", + ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP", "If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false"); configProperties.add(alwaysReadValueFromLDAP); + + ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP", + "If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + configProperties.add(isMandatoryInLdap); } @Override diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 7d9e7ca434..8bca31c624 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -128,7 +128,7 @@ public class UserFederationManager implements UserProvider { if (link != null) { UserModel validatedProxyUser = link.validateAndProxy(realm, user); if (validatedProxyUser != null) { - managedUsers.put(user.getId(), user); + managedUsers.put(user.getId(), validatedProxyUser); return validatedProxyUser; } else { deleteInvalidUser(realm, user); diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java b/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java index e6d465b219..b06348d318 100644 --- a/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationSyncResult.java @@ -8,6 +8,7 @@ public class UserFederationSyncResult { private int added; private int updated; private int removed; + private int failed; public int getAdded() { return added; @@ -33,6 +34,14 @@ public class UserFederationSyncResult { this.removed = removed; } + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + public void increaseAdded() { added++; } @@ -45,14 +54,23 @@ public class UserFederationSyncResult { removed++; } + public void increaseFailed() { + failed++; + } + public void add(UserFederationSyncResult other) { added += other.added; updated += other.updated; removed += other.removed; + failed += other.failed; } public String getStatus() { - return String.format("%d imported users, %d updated users, %d removed users", added, updated, removed); + String status = String.format("%d imported users, %d updated users, %d removed users", added, updated, removed); + if (failed != 0) { + status += String.format(", %d users failed sync! See server log for more details", failed); + } + return status; } @Override diff --git a/pom.xml b/pom.xml index 33794fa206..ca9d82318a 100755 --- a/pom.xml +++ b/pom.xml @@ -155,6 +155,7 @@ testsuite timer export-import + util @@ -431,25 +432,21 @@ org.apache.directory.server apacheds-core-annotations ${apacheds.version} - test org.apache.directory.server apacheds-interceptor-kerberos ${apacheds.version} - test org.apache.directory.server apacheds-server-annotations ${apacheds.version} - test org.apache.directory.api api-ldap-codec-standalone ${apacheds.codec.version} - test @@ -1120,6 +1117,11 @@ ${project.version} zip + + org.keycloak + keycloak-util-embedded-ldap + ${project.version} + org.keycloak keycloak-docs-dist @@ -1184,6 +1186,7 @@ ${project.version} classes + org.keycloak federation-properties-example diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java index 75f609689f..c39bc8a947 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java @@ -1,5 +1,6 @@ package org.keycloak.services; +import org.jboss.logging.Logger; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransactionManager; @@ -11,6 +12,8 @@ import java.util.List; */ public class DefaultKeycloakTransactionManager implements KeycloakTransactionManager { + public static final Logger logger = Logger.getLogger(DefaultKeycloakTransactionManager.class); + private List transactions = new LinkedList(); private List afterCompletion = new LinkedList(); private boolean active; @@ -57,13 +60,26 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan exception = exception == null ? e : exception; } } - for (KeycloakTransaction tx : afterCompletion) { - try { - tx.commit(); - } catch (RuntimeException e) { - exception = exception == null ? e : exception; + + // Don't commit "afterCompletion" if commit of some main transaction failed + if (exception == null) { + for (KeycloakTransaction tx : afterCompletion) { + try { + tx.commit(); + } catch (RuntimeException e) { + exception = exception == null ? e : exception; + } + } + } else { + for (KeycloakTransaction tx : afterCompletion) { + try { + tx.rollback(); + } catch (RuntimeException e) { + logger.error("Exception during rollback", e); + } } } + active = false; if (exception != null) { throw exception; diff --git a/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java b/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java index 1da7bfb9d8..7b6b3ed967 100644 --- a/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java +++ b/services/src/main/java/org/keycloak/services/messages/AdminMessagesProvider.java @@ -29,7 +29,13 @@ public class AdminMessagesProvider implements MessagesProvider { @Override public String getMessage(String messageKey, Object... parameters) { String message = messagesBundle.getProperty(messageKey, messageKey); - return new MessageFormat(message, locale).format(parameters); + + try { + return new MessageFormat(message, locale).format(parameters); + } catch (Exception e) { + logger.warnf("Failed to format message due to: %s", e.getMessage()); + return message; + } } @Override diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 0673780785..cd7c98d535 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -190,40 +190,8 @@ - org.apache.directory.server - apacheds-core-annotations - - - org.slf4j - slf4j-log4j12 - - - org.apache.directory.jdbm - apacheds-jdbm1 - - - - - org.apache.directory.server - apacheds-interceptor-kerberos - - - org.apache.directory.server - apacheds-server-annotations - - - org.slf4j - slf4j-log4j12 - - - org.apache.directory.jdbm - apacheds-jdbm1 - - - - - org.apache.directory.api - api-ldap-codec-standalone + org.keycloak + keycloak-util-embedded-ldap @@ -344,7 +312,7 @@ org.codehaus.mojo exec-maven-plugin - org.keycloak.testsuite.ldap.LDAPEmbeddedServer + org.keycloak.util.ldap.LDAPEmbeddedServer test @@ -359,7 +327,7 @@ org.codehaus.mojo exec-maven-plugin - org.keycloak.testsuite.ldap.KerberosEmbeddedServer + org.keycloak.util.ldap.KerberosEmbeddedServer test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index c4a028c033..1945701fb1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -68,10 +68,10 @@ public class FederationProvidersIntegrationTest { LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); - LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234"); + LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); - LDAPObject existing = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", "5678"); + LDAPObject existing = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678"); } }); @@ -270,7 +270,7 @@ public class FederationProvidersIntegrationTest { RealmModel appRealm = new RealmManager(session).getRealmByName("test"); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", "12398"); + LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", null, "12398"); // Remove default zipcode mapper and add the mapper for "POstalCode" to test case sensitivity UserFederationMapperModel currentZipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper"); @@ -295,7 +295,7 @@ public class FederationProvidersIntegrationTest { RealmModel appRealm = new RealmManager(session).getRealmByName("test"); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", "12399"); + LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", null, "12399"); // Fetch user from LDAP and check that postalCode is filled UserModel user = session.users().getUserByUsername("johndirect", appRealm); @@ -307,9 +307,18 @@ public class FederationProvidersIntegrationTest { johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated"); ldapFedProvider.getLdapIdentityStore().update(johnDirect); + } finally { + keycloakRule.stopSession(session, true); + } + + session = keycloakRule.startSession(); + try { + RealmModel appRealm = new RealmManager(session).getRealmByName("test"); + UserModel user = session.users().getUserByUsername("johndirect", appRealm); + // Verify that postalCode is still the same as we read it's value from Keycloak DB user = session.users().getUserByUsername("johndirect", appRealm); - postalCode = user.getFirstAttribute("postal_code"); + String postalCode = user.getFirstAttribute("postal_code"); Assert.assertEquals("12399", postalCode); // Check user.getAttributes() @@ -370,7 +379,7 @@ public class FederationProvidersIntegrationTest { // Add the user with some fullName into LDAP directly. Ensure that fullName is saved into "cn" attribute in LDAP (currently mapped to model firstName) LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "fullname", "James Dee", "Dee", "fullname@email.org", "4578"); + FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "fullname", "James Dee", "Dee", "fullname@email.org", null, "4578"); // add fullname mapper to the provider and remove "firstNameMapper". For this test, we will simply map full name to the LDAP attribute, which was before firstName ( "givenName" on active directory, "cn" on other LDAP servers) firstNameMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "first name"); @@ -381,9 +390,6 @@ public class FederationProvidersIntegrationTest { FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, ldapFirstNameAttributeName, UserAttributeLDAPFederationMapper.READ_ONLY, "false"); appRealm.addUserFederationMapper(fullNameMapperModel); - - // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName - FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578"); } finally { keycloakRule.stopSession(session, true); } @@ -392,6 +398,9 @@ public class FederationProvidersIntegrationTest { try { RealmModel appRealm = new RealmManager(session).getRealmByName("test"); + // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName + FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578"); + // Remove "fullnameUser" to assert he is removed from LDAP. Revert mappers to previous state UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm); session.users().removeUser(appRealm, fullnameUser); @@ -485,10 +494,10 @@ public class FederationProvidersIntegrationTest { RealmModel appRealm = session.realms().getRealmByName("test"); LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username1", "John1", "Doel1", "user1@email.org", "121"); - FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username2", "John2", "Doel2", "user2@email.org", "122"); - FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username3", "John3", "Doel3", "user3@email.org", "123"); - FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", "124"); + FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username1", "John1", "Doel1", "user1@email.org", null, "121"); + FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username2", "John2", "Doel2", "user2@email.org", null, "122"); + FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username3", "John3", "Doel3", "user3@email.org", null, "123"); + FederationTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", null, "124"); // Users are not at local store at this moment Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm)); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java index 56c4a70b1b..939fe24351 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java @@ -34,7 +34,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; class FederationTestUtils { public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) { - UserModel user = session.users().addUser(realm, username); + UserModel user = session.userStorage().addUser(realm, username); user.setEmail(email); user.setEnabled(true); @@ -47,7 +47,7 @@ class FederationTestUtils { } public static LDAPObject addLDAPUser(LDAPFederationProvider ldapProvider, RealmModel realm, final String username, - final String firstName, final String lastName, final String email, final String postalCode) { + final String firstName, final String lastName, final String email, final String street, final String... postalCode) { UserModel helperUser = new UserModelDelegate(null) { @Override @@ -72,8 +72,10 @@ class FederationTestUtils { @Override public List getAttribute(String name) { - if ("postal_code".equals(name)) { + if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) { return Arrays.asList(postalCode); + } else if ("street".equals(name) && street != null) { + return Arrays.asList(street); } else { return Collections.emptyList(); } @@ -105,7 +107,8 @@ class FederationTestUtils { UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, ldapAttributeName, UserAttributeLDAPFederationMapper.READ_ONLY, "false", - UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false"); + UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false", + UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); realm.addUserFederationMapper(mapperModel); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java index e1e9aff5c7..edd210cb41 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java @@ -1,6 +1,9 @@ package org.keycloak.testsuite.federation; import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -17,6 +20,7 @@ import org.junit.runners.MethodSorters; import org.keycloak.OAuth2Constants; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; @@ -60,6 +64,19 @@ public class LDAPMultipleAttributesTest { FederationTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel); FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET); + // Remove current users and add default users + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); + + LDAPObject james = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", null, "88441"); + ldapFedProvider.getLdapIdentityStore().updatePassword(james, "password"); + + // User for testing duplicating surname and postalCode + LDAPObject bruce = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "bwilson", "Bruce", "Wilson", "bwilson@keycloak.org", "Elm 5", "88441", "77332"); + bruce.setAttribute("sn", new LinkedHashSet<>(Arrays.asList("Wilson", "Schneider"))); + ldapFedProvider.getLdapIdentityStore().update(bruce); + ldapFedProvider.getLdapIdentityStore().updatePassword(bruce, "password"); + // Create ldap-portal client ClientModel ldapClient = appRealm.addClient("ldap-portal"); ldapClient.addRedirectUri("/ldap-portal"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java index 4802105107..c0a8cd652d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java @@ -76,13 +76,13 @@ public class LDAPRoleMappingsTest { FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper"); // Add some users for testing - LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234"); + LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); - LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", "5678"); + LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678"); ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1"); - LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", "8910"); + LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910"); ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1"); // Add some roles for testing diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java similarity index 99% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java index e5d8408f90..31b8d2b804 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPTestConfiguration.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.ldap; +package org.keycloak.testsuite.federation; import java.io.File; import java.io.InputStream; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index b463bcdfbd..e29a2b8e9a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -61,7 +61,7 @@ public class SyncProvidersTest { FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm); for (int i=1 ; i<=5 ; i++) { - LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", "12" + i); + LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i); ldapFedProvider.getLdapIdentityStore().updatePassword(ldapUser, "Password1"); } @@ -81,7 +81,7 @@ public class SyncProvidersTest { // } @Test - public void testLDAPSync() { + public void test01LDAPSync() { UsersSyncManager usersSyncManager = new UsersSyncManager(); // wait a bit @@ -91,7 +91,7 @@ public class SyncProvidersTest { try { KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); UserFederationSyncResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel); - assertSyncEquals(syncResult, 5, 0, 0); + assertSyncEquals(syncResult, 5, 0, 0, 0); } finally { keycloakRule.stopSession(session, false); } @@ -123,7 +123,7 @@ public class SyncProvidersTest { // Add user to LDAP and update 'user5' in LDAP LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); - FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126"); + FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", null, "126"); LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5"); // NOTE: Changing LDAP attributes directly here ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org"); @@ -137,7 +137,7 @@ public class SyncProvidersTest { // Trigger partial sync KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); UserFederationSyncResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel); - assertSyncEquals(syncResult, 1, 1, 0); + assertSyncEquals(syncResult, 1, 1, 0, 0); } finally { keycloakRule.stopSession(session, false); } @@ -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); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/EmbeddedServersFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/EmbeddedServersFactory.java deleted file mode 100644 index 40106b5df7..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/EmbeddedServersFactory.java +++ /dev/null @@ -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 Marek Posolda - */ -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); - } -} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPEmbeddedServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPEmbeddedServer.java deleted file mode 100644 index 2fc7028943..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPEmbeddedServer.java +++ /dev/null @@ -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 Marek Posolda - */ -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 map = new HashMap(); - 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()); - } - -} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java index 9026557313..57bf79ac29 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java @@ -2,11 +2,12 @@ package org.keycloak.testsuite.rule; import java.io.File; import java.net.URL; +import java.util.Properties; import org.jboss.logging.Logger; -import org.keycloak.testsuite.ldap.EmbeddedServersFactory; -import org.keycloak.testsuite.ldap.LDAPTestConfiguration; -import org.keycloak.testsuite.ldap.LDAPEmbeddedServer; +import org.keycloak.testsuite.federation.LDAPTestConfiguration; +import org.keycloak.util.ldap.KerberosEmbeddedServer; +import org.keycloak.util.ldap.LDAPEmbeddedServer; /** * @author Marek Posolda @@ -33,7 +34,11 @@ public class KerberosRule extends LDAPRule { } @Override - protected LDAPEmbeddedServer createServer(EmbeddedServersFactory factory) { - return factory.createKerberosServer(); + protected LDAPEmbeddedServer createServer() { + Properties defaultProperties = new Properties(); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:kerberos/users-kerberos.ldif"); + + return new KerberosEmbeddedServer(defaultProperties); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java index 1bf9b8e69c..add9708380 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java @@ -1,11 +1,11 @@ package org.keycloak.testsuite.rule; import java.util.Map; +import java.util.Properties; import org.junit.rules.ExternalResource; -import org.keycloak.testsuite.ldap.EmbeddedServersFactory; -import org.keycloak.testsuite.ldap.LDAPTestConfiguration; -import org.keycloak.testsuite.ldap.LDAPEmbeddedServer; +import org.keycloak.testsuite.federation.LDAPTestConfiguration; +import org.keycloak.util.ldap.LDAPEmbeddedServer; /** * @author Marek Posolda @@ -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 getConfig() { diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif index 4d6d87ee0c..176e19b81a 100644 --- a/testsuite/integration/src/test/resources/ldap/users.ldif +++ b/testsuite/integration/src/test/resources/ldap/users.ldif @@ -18,30 +18,3 @@ dn: ou=FinanceRoles,dc=keycloak,dc=org objectclass: top objectclass: organizationalUnit ou: FinanceRoles - -dn: uid=jbrown,ou=People,dc=keycloak,dc=org -objectclass: top -objectclass: person -objectclass: organizationalPerson -objectclass: inetOrgPerson -uid: jbrown -cn: James -sn: Brown -mail: jbrown@keycloak.org -postalCode: 88441 -userPassword: password - -dn: uid=bwilson,ou=People,dc=keycloak,dc=org -objectclass: top -objectclass: person -objectclass: organizationalPerson -objectclass: inetOrgPerson -uid: bwilson -cn: Bruce -sn: Wilson -sn: Schneider -mail: bwilson@keycloak.org -postalCode: 88441 -postalCode: 77332 -street: Elm 5 -userPassword: password diff --git a/util/embedded-ldap/pom.xml b/util/embedded-ldap/pom.xml new file mode 100644 index 0000000000..5a1e927309 --- /dev/null +++ b/util/embedded-ldap/pom.xml @@ -0,0 +1,91 @@ + + + + keycloak-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-util-embedded-ldap + Keycloak Util Embedded LDAP + + + + + + org.keycloak + keycloak-core + + + org.jboss.logging + jboss-logging + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + compile + + + + org.apache.directory.server + apacheds-core-annotations + + + org.slf4j + slf4j-log4j12 + + + org.apache.directory.jdbm + apacheds-jdbm1 + + + + + org.apache.directory.server + apacheds-interceptor-kerberos + + + org.apache.directory.server + apacheds-server-annotations + + + org.slf4j + slf4j-log4j12 + + + org.apache.directory.jdbm + apacheds-jdbm1 + + + + + org.apache.directory.api + api-ldap-codec-standalone + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java new file mode 100644 index 0000000000..f0153ec67c --- /dev/null +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/FileDirectoryServiceFactory.java @@ -0,0 +1,267 @@ +package org.keycloak.util.ldap; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.directory.api.ldap.model.constants.SchemaConstants; +import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.schema.LdapComparator; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.model.schema.comparators.NormalizingComparator; +import org.apache.directory.api.ldap.model.schema.registries.ComparatorRegistry; +import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader; +import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader; +import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager; +import org.apache.directory.api.util.exception.Exceptions; +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.api.CacheService; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.InstanceLayout; +import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.api.schema.SchemaPartition; +import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory; +import org.apache.directory.server.core.factory.DirectoryServiceFactory; +import org.apache.directory.server.core.factory.JdbmPartitionFactory; +import org.apache.directory.server.core.factory.LdifPartitionFactory; +import org.apache.directory.server.core.factory.PartitionFactory; +import org.apache.directory.server.core.partition.ldif.LdifPartition; +import org.apache.directory.server.i18n.I18n; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Slightly modified version of {@link DefaultDirectoryServiceFactory} which allows persistence among restarts and uses LDIF partitions by default + * + * @author Marek Posolda + */ +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 type = ( Class ) 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 errors = schemaManager.getErrors(); + + if ( errors.size() != 0 ) + { + throw new Exception( I18n.err(I18n.ERR_317, Exceptions.printErrors(errors)) ); + } + } + + + /** + * Inits the system partition. + * + * @throws Exception the exception + */ + private void initSystemPartition() throws Exception + { + // change the working directory to something that is unique + // on the system and somewhere either under target directory + // or somewhere in a temp area of the machine. + + // Inject the System Partition + Partition systemPartition = partitionFactory.createPartition( directoryService.getSchemaManager(), + directoryService.getDnFactory(), + "system", ServerDNConstants.SYSTEM_DN, 500, + new File( directoryService.getInstanceLayout().getPartitionsDirectory(), "system" ) ); + systemPartition.setSchemaManager(directoryService.getSchemaManager()); + + partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100 ); + + directoryService.setSystemPartition( systemPartition ); + } + + + /** + * Builds the directory server instance. + * + * @param name the instance name + */ + private void build( String name ) throws Exception + { + directoryService.setInstanceId( name ); + buildInstanceDirectory( name ); + + CacheService cacheService = new CacheService(); + cacheService.initialize( directoryService.getInstanceLayout() ); + + directoryService.setCacheService( cacheService ); + + // Init the service now + initSchema(); + initSystemPartition(); + + directoryService.startup(); + } + + + /** + * {@inheritDoc} + */ + public DirectoryService getDirectoryService() throws Exception + { + return directoryService; + } + + + /** + * {@inheritDoc} + */ + public PartitionFactory getPartitionFactory() throws Exception + { + return partitionFactory; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemoryDirectoryServiceFactory.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java similarity index 98% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemoryDirectoryServiceFactory.java rename to util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java index a5eb2aad11..f5fd080a4f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemoryDirectoryServiceFactory.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java @@ -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()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemorySchemaPartition.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemorySchemaPartition.java similarity index 95% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemorySchemaPartition.java rename to util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemorySchemaPartition.java index 227d257abd..ff37924b30 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/InMemorySchemaPartition.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemorySchemaPartition.java @@ -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()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java similarity index 71% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosEmbeddedServer.java rename to util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java index 325f19b4ec..a997ee0f2e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosEmbeddedServer.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java @@ -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 Marek Posolda @@ -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()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosKeytabCreator.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java similarity index 87% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosKeytabCreator.java rename to util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java index 7631019d63..8863e7c531 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/KerberosKeytabCreator.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java @@ -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: "); 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. * diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java new file mode 100644 index 0000000000..18a1a30ed0 --- /dev/null +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java @@ -0,0 +1,272 @@ +package org.keycloak.util.ldap; + +import java.io.File; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.text.StrSubstitutor; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.factory.DirectoryServiceFactory; +import org.apache.directory.server.core.factory.PartitionFactory; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.protocol.shared.transport.Transport; +import org.jboss.logging.Logger; +import org.keycloak.util.FindFile; +import org.keycloak.util.StreamUtil; + +/** + * @author Marek Posolda + */ +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 map = new HashMap(); + map.put("hostname", this.bindHost); + if (this.ldapSaslPrincipal != null) { + map.put("ldapSaslPrincipal", this.ldapSaslPrincipal); + } + + // Find LDIF file on filesystem or classpath ( if it's like classpath:ldap/users.ldif ) + InputStream is = FindFile.findFile(ldifFile); + if (is == null) { + throw new IllegalStateException("LDIF file not found on classpath or on file system. Location was: " + ldifFile); + } + + final String ldifContent = StrSubstitutor.replace(StreamUtil.readString(is), map); + log.info("Content of LDIF: " + ldifContent); + final SchemaManager schemaManager = directoryService.getSchemaManager(); + + importLdifContent(directoryService, ldifContent); + } + + private static void importLdifContent(DirectoryService directoryService, String ldifContent) throws Exception { + LdifReader ldifReader = new LdifReader(IOUtils.toInputStream(ldifContent)); + + try { + for (LdifEntry ldifEntry : ldifReader) { + try { + directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry())); + } catch (LdapEntryAlreadyExistsException ignore) { + log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring"); + } + } + } finally { + ldifReader.close(); + } + } + + + public void stop() throws Exception { + stopLdapServer(); + shutdownDirectoryService(); + } + + + protected void stopLdapServer() { + log.info("Stopping LDAP server."); + ldapServer.stop(); + } + + + protected void shutdownDirectoryService() throws Exception { + log.info("Stopping Directory service."); + directoryService.shutdown(); + + // Delete workfiles just for 'inmemory' implementation used in tests. Normally we want LDAP data to persist + File instanceDir = directoryService.getInstanceLayout().getInstanceDirectory(); + if (this.directoryServiceFactory.equals(DSF_INMEMORY)) { + log.infof("Removing Directory service workfiles: %s", instanceDir.getAbsolutePath()); + FileUtils.deleteDirectory(instanceDir); + } else { + log.info("Working LDAP directory not deleted. Delete it manually if you want to start with fresh LDAP data. Directory location: " + instanceDir.getAbsolutePath()); + } + } + +} diff --git a/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif new file mode 100644 index 0000000000..fd9936cfa9 --- /dev/null +++ b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif @@ -0,0 +1,90 @@ +dn: dc=keycloak,dc=org +objectclass: dcObject +objectclass: organization +o: Keycloak +dc: Keycloak + +dn: ou=People,dc=keycloak,dc=org +objectClass: organizationalUnit +objectClass: top +ou: People + +dn: uid=krbtgt,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: KDC Service +sn: Service +uid: krbtgt +userPassword: secret +krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=ldap,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: LDAP +sn: Service +uid: ldap +userPassword: randall +krb5PrincipalName: ${ldapSaslPrincipal} +krb5KeyVersionNumber: 0 + +dn: uid=HTTP,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: HTTP +sn: Service +uid: HTTP +userPassword: httppwd +krb5PrincipalName: HTTP/${hostname}@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=hnelson,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: Horatio +sn: Nelson +mail: hnelson@keycloak.org +uid: hnelson +userPassword: secret +krb5PrincipalName: hnelson@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=jduke,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: Java +sn: Duke +mail: jduke@keycloak.org +uid: jduke +userPassword: theduke +krb5PrincipalName: jduke@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=gsstestserver,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: gsstestserver +sn: Service +uid: gsstestserver +userPassword: gsstestpwd +krb5PrincipalName: gsstestserver/xxx@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 diff --git a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif new file mode 100644 index 0000000000..4d6d87ee0c --- /dev/null +++ b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif @@ -0,0 +1,47 @@ +dn: dc=keycloak,dc=org +objectclass: dcObject +objectclass: organization +o: Keycloak +dc: Keycloak + +dn: ou=People,dc=keycloak,dc=org +objectclass: top +objectclass: organizationalUnit +ou: People + +dn: ou=RealmRoles,dc=keycloak,dc=org +objectclass: top +objectclass: organizationalUnit +ou: RealmRoles + +dn: ou=FinanceRoles,dc=keycloak,dc=org +objectclass: top +objectclass: organizationalUnit +ou: FinanceRoles + +dn: uid=jbrown,ou=People,dc=keycloak,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: jbrown +cn: James +sn: Brown +mail: jbrown@keycloak.org +postalCode: 88441 +userPassword: password + +dn: uid=bwilson,ou=People,dc=keycloak,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: bwilson +cn: Bruce +sn: Wilson +sn: Schneider +mail: bwilson@keycloak.org +postalCode: 88441 +postalCode: 77332 +street: Elm 5 +userPassword: password diff --git a/util/embedded-ldap/src/main/resources/log4j.properties b/util/embedded-ldap/src/main/resources/log4j.properties new file mode 100644 index 0000000000..37d89b14b6 --- /dev/null +++ b/util/embedded-ldap/src/main/resources/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootLogger=info, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n + +log4j.logger.org.keycloak=info +log4j.logger.org.apache.directory.api=warn +log4j.logger.org.apache.directory.server.core=warn \ No newline at end of file diff --git a/util/pom.xml b/util/pom.xml new file mode 100644 index 0000000000..3026307068 --- /dev/null +++ b/util/pom.xml @@ -0,0 +1,22 @@ + + + keycloak-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + ../pom.xml + + + Keycloak Util Parent + + 4.0.0 + + org.keycloak + keycloak-util-parent + pom + + + embedded-ldap + + + \ No newline at end of file