keycloak-uncached

KEYCLOAK-2403 Cannot create user in LDAP/AD from Keycloak

1/20/2017 6:08:13 PM

Details

diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java
index 8bfbf6d..94014fa 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPDn.java
@@ -27,7 +27,15 @@ import java.util.LinkedList;
  */
 public class LDAPDn {
 
-    private final Deque<Entry> entries = new LinkedList<>();
+    private final Deque<Entry> entries;
+
+    private LDAPDn() {
+        this.entries = new LinkedList<>();
+    }
+
+    private LDAPDn(Deque<Entry> entries) {
+        this.entries = entries;
+    }
 
     public static LDAPDn fromString(String dnString) {
         LDAPDn dn = new LDAPDn();
@@ -115,12 +123,14 @@ public class LDAPDn {
 
     /**
      *
-     * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
+     * @return DN like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org".
+     * Returned DN will be new clone not related to the original DN instance.
+     *
      */
-    public String getParentDn() {
+    public LDAPDn getParentDn() {
         LinkedList<Entry> parentDnEntries = new LinkedList<>(entries);
         parentDnEntries.remove();
-        return toString(parentDnEntries);
+        return new LDAPDn(parentDnEntries);
     }
 
     public boolean isDescendantOf(LDAPDn expectedParentDn) {
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
index c0e84b0..249a3a0 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -103,6 +103,8 @@ public class LDAPIdentityStore implements IdentityStore {
 
     @Override
     public void update(LDAPObject ldapObject) {
+        checkRename(ldapObject);
+
         BasicAttributes updatedAttributes = extractAttributes(ldapObject, false);
         NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
 
@@ -114,6 +116,33 @@ public class LDAPIdentityStore implements IdentityStore {
         }
     }
 
+    protected void checkRename(LDAPObject ldapObject) {
+        String rdnAttrName = ldapObject.getRdnAttributeName();
+        if (ldapObject.getReadOnlyAttributeNames().contains(rdnAttrName.toLowerCase())) {
+            return;
+        }
+
+        String rdnAttrVal = ldapObject.getAttributeAsString(rdnAttrName);
+
+        String oldRdnAttrVal = ldapObject.getDn().getFirstRdnAttrValue();
+        if (!oldRdnAttrVal.equals(rdnAttrVal)) {
+            LDAPDn newLdapDn = ldapObject.getDn().getParentDn();
+            newLdapDn.addFirst(rdnAttrName, rdnAttrVal);
+
+            String oldDn = ldapObject.getDn().toString();
+            String newDn = newLdapDn.toString();
+
+            if (logger.isDebugEnabled()) {
+                logger.debugf("Renaming LDAP Object. Old DN: [%s], New DN: [%s]", oldDn, newDn);
+            }
+
+            // In case, that there is conflict (For example already existing "CN=John Anthony"), the different DN is returned
+            newDn = this.operationManager.renameEntry(oldDn, newDn, true);
+
+            ldapObject.setDn(LDAPDn.fromString(newDn));
+        }
+    }
+
     @Override
     public void remove(LDAPObject ldapObject) {
         this.operationManager.removeEntry(ldapObject.getDn().toString());
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
index 350b16d..f057fc8 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -21,6 +21,7 @@ import org.jboss.logging.Logger;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.idm.model.LDAPDn;
 import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
 
@@ -28,6 +29,7 @@ import javax.naming.AuthenticationException;
 import javax.naming.Binding;
 import javax.naming.Context;
 import javax.naming.InitialContext;
+import javax.naming.NameAlreadyBoundException;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.directory.Attribute;
@@ -158,6 +160,64 @@ public class LDAPOperationManager {
         }
     }
 
+
+    /**
+     * Rename LDAPObject name (DN)
+     *
+     * @param oldDn
+     * @param newDn
+     * @param fallback With fallback=true, we will try to find the another DN in case of conflict. For example if there is an
+     *                 attempt to rename to "CN=John Doe", but there is already existing "CN=John Doe", we will try "CN=John Doe0"
+     * @return the non-conflicting DN, which was used in the end
+     */
+    public String renameEntry(String oldDn, String newDn, boolean fallback) {
+        try {
+            String newNonConflictingDn = execute(new LdapOperation<String>() {
+                @Override
+                public String execute(LdapContext context) throws NamingException {
+                    String dn = newDn;
+
+                    // Max 5 attempts for now
+                    int max = 5;
+                    for (int i=0 ; i<max ; i++) {
+                        try {
+                            context.rename(oldDn, dn);
+                            return dn;
+                        } catch (NameAlreadyBoundException ex) {
+                            if (!fallback) {
+                                throw ex;
+                            } else {
+                                String failedDn = dn;
+                                if (i<max) {
+                                    dn = findNextDNForFallback(newDn, i);
+                                    logger.warnf("Failed to rename DN [%s] to [%s]. Will try to fallback to DN [%s]", oldDn, failedDn, dn);
+                                } else {
+                                    logger.warnf("Failed all fallbacks for renaming [%s]", oldDn);
+                                    throw ex;
+                                }
+                            }
+                        }
+                    }
+
+                    throw new ModelException("Could not rename entry from DN [" + oldDn + "] to new DN [" + newDn + "]. All fallbacks failed");
+                }
+            });
+            return newNonConflictingDn;
+        } catch (NamingException e) {
+            throw new ModelException("Could not rename entry from DN [" + oldDn + "] to new DN [" + newDn + "]", e);
+        }
+    }
+
+    private String findNextDNForFallback(String newDn, int counter) {
+        LDAPDn dn = LDAPDn.fromString(newDn);
+        String rdnAttrName = dn.getFirstRdnAttrName();
+        String rdnAttrVal = dn.getFirstRdnAttrValue();
+        LDAPDn parentDn = dn.getParentDn();
+        parentDn.addFirst(rdnAttrName, rdnAttrVal + counter);
+        return parentDn.toString();
+    }
+
+
     public List<SearchResult> search(final String baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
         final List<SearchResult> result = new ArrayList<SearchResult>();
         final SearchControls cons = getSearchControls(returningAttributes, searchScope);
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapper.java
index 43d631e..223a115 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapper.java
@@ -75,7 +75,7 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
     @Override
     public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
         String ldapFullNameAttrName = getLdapFullNameAttrName();
-        String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
+        String fullName = getFullNameForWriteToLDAP(localUser.getFirstName(), localUser.getLastName(), localUser.getUsername());
         ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
 
         if (isReadOnly()) {
@@ -103,7 +103,7 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
                 }
 
                 private void setFullNameToLDAPObject() {
-                    String fullName = getFullName(getFirstName(), getLastName());
+                    String fullName = getFullNameForWriteToLDAP(getFirstName(), getLastName(), getUsername());
                     if (logger.isTraceEnabled()) {
                         logger.tracef("Pushing full name attribute to LDAP. Full name: %s", fullName);
                     }
@@ -177,18 +177,24 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
         return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
     }
 
-    protected String getFullName(String firstName, String lastName) {
-        if (firstName != null && lastName != null) {
+    protected String getFullNameForWriteToLDAP(String firstName, String lastName, String username) {
+        if (!isBlank(firstName) && !isBlank(lastName)) {
             return firstName + " " + lastName;
-        } else if (firstName != null) {
+        } else if (!isBlank(firstName)) {
             return firstName;
-        } else if (lastName != null) {
+        } else if (!isBlank(lastName)) {
             return lastName;
+        } else if (isFallbackToUsername()) {
+            return username;
         } else {
             return LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
         }
     }
 
+    private boolean isBlank(String attr) {
+        return attr == null || attr.trim().isEmpty();
+    }
+
     private boolean isReadOnly() {
         return parseBooleanParameter(mapperModel, READ_ONLY);
     }
@@ -196,4 +202,11 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
     private boolean isWriteOnly() {
         return parseBooleanParameter(mapperModel, WRITE_ONLY);
     }
+
+
+    // Used just in case that we have Writable LDAP and fullName is mapped to "cn", which is used as RDN (this typically happens only on MSAD)
+    private boolean isFallbackToUsername() {
+        String rdnLdapAttrConfig = getLdapProvider().getLdapIdentityStore().getConfig().getRdnLdapAttribute();
+        return !isReadOnly() && getLdapFullNameAttrName().equalsIgnoreCase(rdnLdapAttrConfig);
+    }
 }
diff --git a/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java
index a668cd7..d749c13 100644
--- a/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java
+++ b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPDnTest.java
@@ -35,7 +35,7 @@ public class LDAPDnTest {
         Assert.assertEquals("uid=Johny\\,Depp\\+Pepp\\\\Foo,ou=People,dc=keycloak,dc=org", dn.toString());
         Assert.assertEquals(LDAPDn.fromString("uid=Johny\\,Depp\\+Pepp\\\\Foo,ou=People,dc=keycloak,dc=org"), dn);
 
-        Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
+        Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn().toString());
 
         Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=keycloak, dc=org")));
         Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=org")));
@@ -46,4 +46,22 @@ public class LDAPDnTest {
         Assert.assertEquals("uid", dn.getFirstRdnAttrName());
         Assert.assertEquals("Johny,Depp+Pepp\\Foo", dn.getFirstRdnAttrValue());
     }
+
+    @Test
+    public void testCorrectEscape() throws Exception {
+        LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
+        dn.addFirst("cn", "Johny,Džýa Foo");
+        Assert.assertEquals("cn=Johny\\,Džýa Foo,dc=keycloak,dc=org", dn.toString());
+        Assert.assertEquals("Johny,Džýa Foo", dn.getFirstRdnAttrValue());
+
+        dn = LDAPDn.fromString("dc=keycloak, dc=org");
+        dn.addFirst("cn", "Johny,Džýa Foo ");
+        Assert.assertEquals("cn=Johny\\,Džýa Foo\\ ,dc=keycloak,dc=org", dn.toString());
+        Assert.assertEquals("Johny,Džýa Foo ", dn.getFirstRdnAttrValue());
+
+        dn = LDAPDn.fromString("dc=keycloak, dc=org");
+        dn.addFirst("cn", "Johny,Džýa ");
+        Assert.assertEquals("cn=Johny\\,Džýa\\ ,dc=keycloak,dc=org", dn.toString());
+        Assert.assertEquals("Johny,Džýa ", dn.getFirstRdnAttrValue());
+    }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADFullNameTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADFullNameTest.java
new file mode 100644
index 0000000..fe6510d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADFullNameTest.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.federation.storage.ldap;
+
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountPasswordPage;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * Test for the MSAD setup with usernameAttribute=sAMAccountName, rdnAttribute=cn and fullNameMapper mapped to cn
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class MSADFullNameTest {
+
+    // Run this test just on MSAD and just when sAMAccountName is mapped to username
+    private static LDAPRule ldapRule = new LDAPRule((Map<String, String> ldapConfig) -> {
+
+        String vendor = ldapConfig.get(LDAPConstants.VENDOR);
+        if (!vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY)) {
+            return true;
+        }
+
+        String usernameAttr = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
+        return !usernameAttr.equalsIgnoreCase(LDAPConstants.SAM_ACCOUNT_NAME);
+
+    });
+
+    private static ComponentModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "marykeycloak", "mary@test.com", "password-app");
+
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(1);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.getConfig().addAll(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+            LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+
+            // Delete all LDAP users and add some new for testing
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            // Remove the mapper for "username-cn" and create new mapper for fullName
+            ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ldapModel, "username-cn");
+            Assert.assertNotNull(mapperModel);
+            appRealm.removeComponent(mapperModel);
+
+            mapperModel = KeycloakModelUtils.createComponentModel("fullNameWritable", ldapModel.getId(), FullNameLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
+                    FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
+                    FullNameLDAPStorageMapper.READ_ONLY, "false",
+                    FullNameLDAPStorageMapper.WRITE_ONLY, "true");
+            appRealm.addComponentModel(mapperModel);
+
+            appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
+        }
+    });
+
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected AppPage appPage;
+
+    @WebResource
+    protected RegisterPage registerPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @WebResource
+    protected AccountUpdateProfilePage profilePage;
+
+    @WebResource
+    protected AccountPasswordPage changePasswordPage;
+
+
+
+//    @Test
+//    public void test01Sleep() throws Exception {
+//        Thread.sleep(1000000);
+//    }
+
+    @Test
+    public void test01_addUserWithoutFullName() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().addUser(appRealm, "johnkeycloak");
+            john.setEmail("johnkeycloak@email.cz");
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            Assert.assertNotNull(john.getFederationLink());
+            assertDnStartsWith(session, john, "cn=johnkeycloak");
+
+            session.users().removeUser(appRealm, john);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+    }
+
+
+    @Test
+    public void test02_registerUserWithFullName() {
+
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.assertCurrent();
+
+        registerPage.register("Johny", "Anthony", "johnyanth@check.cz", "johnkeycloak", "Password1", "Password1");
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            assertUser(session, john, "johnkeycloak", "Johny", "Anthony", true, "cn=Johny Anthony");
+
+            session.users().removeUser(appRealm, john);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void test03_addUserWithFirstNameOnly() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().addUser(appRealm, "johnkeycloak");
+            john.setEmail("johnkeycloak@email.cz");
+            john.setFirstName("Johnyyy");
+            john.setEnabled(true);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            assertUser(session, john, "johnkeycloak", "Johnyyy", "", true, "cn=Johnyyy");
+
+            session.users().removeUser(appRealm, john);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void test04_addUserWithLastNameOnly() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().addUser(appRealm, "johnkeycloak");
+            john.setEmail("johnkeycloak@email.cz");
+            john.setLastName("Anthonyy");
+            john.setEnabled(true);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            assertUser(session, john, "johnkeycloak", "", "Anthonyy", true, "cn=Anthonyy");
+
+            session.users().removeUser(appRealm, john);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void test05_registerUserWithFullNameSpecialChars() {
+
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.assertCurrent();
+
+        registerPage.register("Jož,o", "Baříč", "johnyanth@check.cz", "johnkeycloak", "Password1", "Password1");
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            assertUser(session, john, "johnkeycloak", "Jož,o", "Baříč", true, "cn=Jož\\,o Baříč");
+
+            session.users().removeUser(appRealm, john);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void test06_conflicts() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel john = session.users().addUser(appRealm, "existingkc");
+            john.setFirstName("John");
+            john.setLastName("Existing");
+            john.setEnabled(true);
+
+            UserModel john2 = session.users().addUser(appRealm, "existingkc1");
+            john2.setEnabled(true);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.assertCurrent();
+
+        registerPage.register("John", "Existing", "johnyanth@check.cz", "existingkc", "Password1", "Password1");
+        Assert.assertEquals("Username already exists.", registerPage.getError());
+
+        registerPage.register("John", "Existing", "johnyanth@check.cz", "existingkc2", "Password1", "Password1");
+        appPage.logout();
+
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.assertCurrent();
+        registerPage.register("John", "Existing", "johnyanth2@check.cz", "existingkc3", "Password1", "Password1");
+
+        session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+            RealmModel appRealm = manager.getRealm("test");
+
+            UserModel existingKc = session.users().getUserByUsername("existingkc", appRealm);
+            assertUser(session, existingKc, "existingkc", "John", "Existing", true, "cn=John Existing");
+
+            UserModel existingKc1 = session.users().getUserByUsername("existingkc1", appRealm);
+            assertUser(session, existingKc1, "existingkc1", "", "", true, "cn=existingkc1");
+
+            UserModel existingKc2 = session.users().getUserByUsername("existingkc2", appRealm);
+            assertUser(session, existingKc2, "existingkc2", "John", "Existing", true, "cn=John Existing0");
+
+            UserModel existingKc3 = session.users().getUserByUsername("existingkc3", appRealm);
+            assertUser(session, existingKc3, "existingkc3", "John", "Existing", true, "cn=John Existing1");
+
+            session.users().removeUser(appRealm, existingKc);
+            session.users().removeUser(appRealm, existingKc1);
+            session.users().removeUser(appRealm, existingKc2);
+            session.users().removeUser(appRealm, existingKc3);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    private void assertUser(KeycloakSession session, UserModel user, String expectedUsername, String expectedFirstName, String expectedLastName, boolean expectedEnabled, String expectedDn) {
+        Assert.assertNotNull(user);
+        Assert.assertNotNull(user.getFederationLink());
+        Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
+        Assert.assertEquals(expectedUsername, user.getUsername());
+        Assert.assertEquals(expectedFirstName, user.getFirstName());
+        Assert.assertEquals(expectedLastName, user.getLastName());
+        Assert.assertEquals(expectedEnabled, user.isEnabled());
+        assertDnStartsWith(session, user, expectedDn);
+    }
+
+
+    private void assertDnStartsWith(KeycloakSession session, UserModel user, String expectedRDn) {
+        String usersDn = LDAPTestUtils.getLdapProvider(session, ldapModel).getLdapIdentityStore().getConfig().getUsersDn();
+        String userDN = user.getFirstAttribute(LDAPConstants.LDAP_ENTRY_DN);
+        Assert.assertTrue(userDN.equalsIgnoreCase(expectedRDn + "," + usersDn));
+    }
+
+}