keycloak-uncached

Details

diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
index 8edb6ca..7e112b1 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
@@ -92,6 +92,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
     protected LDAPProviderKerberosConfig kerberosConfig;
     protected PasswordUpdateCallback updater;
     protected LDAPStorageMapperManager mapperManager;
+    protected LDAPStorageUserManager userManager;
 
     protected final Set<String> supportedCredentialTypes = new HashSet<>();
 
@@ -103,6 +104,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
         this.kerberosConfig = new LDAPProviderKerberosConfig(model);
         this.editMode = ldapIdentityStore.getConfig().getEditMode();
         this.mapperManager = new LDAPStorageMapperManager(this);
+        this.userManager = new LDAPStorageUserManager(this);
 
         supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
         if (kerberosConfig.isAllowKerberosAuthentication()) {
@@ -134,6 +136,11 @@ public class LDAPStorageProvider implements UserStorageProvider,
         return mapperManager;
     }
 
+    public LDAPStorageUserManager getUserManager() {
+        return userManager;
+    }
+
+
     @Override
     public UserModel validate(RealmModel realm, UserModel local) {
         LDAPObject ldapObject = loadAndValidateUser(realm, local);
@@ -145,6 +152,11 @@ public class LDAPStorageProvider implements UserStorageProvider,
     }
 
     protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject) {
+        UserModel existing = userManager.getManagedProxiedUser(local.getId());
+        if (existing != null) {
+            return existing;
+        }
+
         UserModel proxied = local;
 
         checkDNChanged(realm, local, ldapObject);
@@ -167,6 +179,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
             proxied = ldapMapper.proxy(ldapObject, proxied, realm);
         }
 
+        userManager.setManagedProxiedUser(proxied, ldapObject);
+
         return proxied;
     }
 
@@ -227,6 +241,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
         }
 
         ldapIdentityStore.remove(ldapObject);
+        userManager.removeManagedUserEntry(user.getId());
+
         return true;
     }
 
@@ -385,6 +401,11 @@ public class LDAPStorageProvider implements UserStorageProvider,
      * @return ldapUser corresponding to local user or null if user is no longer in LDAP
      */
     protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) {
+        LDAPObject existing = userManager.getManagedLDAPUser(local.getId());
+        if (existing != null) {
+            return existing;
+        }
+
         LDAPObject ldapUser = loadLDAPUserByUsername(realm, local.getUsername());
         if (ldapUser == null) {
             return null;
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageUserManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageUserManager.java
new file mode 100644
index 0000000..5155c12
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageUserManager.java
@@ -0,0 +1,103 @@
+/*
+ * 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.storage.ldap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.LDAPTransaction;
+
+/**
+ * Track which LDAP users were already enlisted during this transaction
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPStorageUserManager {
+
+    private final Map<String, ManagedUserEntry> managedUsers = new HashMap<>();
+    private final LDAPStorageProvider provider;
+
+    public LDAPStorageUserManager(LDAPStorageProvider provider) {
+        this.provider = provider;
+    }
+
+    public UserModel getManagedProxiedUser(String userId) {
+        ManagedUserEntry entry = managedUsers.get(userId);
+        return entry==null ? null : entry.getManagedProxiedUser();
+    }
+
+    public LDAPObject getManagedLDAPUser(String userId) {
+        ManagedUserEntry entry = managedUsers.get(userId);
+        return entry==null ? null : entry.getLdapUser();
+    }
+
+    public LDAPTransaction getTransaction(String userId) {
+        ManagedUserEntry entry = managedUsers.get(userId);
+        if (entry == null) {
+            throw new IllegalStateException("Shouldn't happen to not have entry for userId: " + userId);
+        }
+
+        return entry.getLdapTransaction();
+
+    }
+
+    public void setManagedProxiedUser(UserModel proxiedUser, LDAPObject ldapObject) {
+        String userId = proxiedUser.getId();
+        ManagedUserEntry entry = managedUsers.get(userId);
+        if (entry != null) {
+            throw new IllegalStateException("Don't expect to have entry for user " + userId);
+        }
+
+        LDAPTransaction ldapTransaction = new LDAPTransaction(provider, ldapObject);
+        ManagedUserEntry newEntry = new ManagedUserEntry(proxiedUser, ldapObject, ldapTransaction);
+        managedUsers.put(userId, newEntry);
+    }
+
+    public void removeManagedUserEntry(String userId) {
+        managedUsers.remove(userId);
+    }
+
+
+
+    private static class ManagedUserEntry {
+
+        private final UserModel managedProxiedUser;
+        private final LDAPObject ldapUser;
+        private final LDAPTransaction ldapTransaction;
+
+        public ManagedUserEntry(UserModel managedProxiedUser, LDAPObject ldapUser, LDAPTransaction ldapTransaction) {
+            this.managedProxiedUser = managedProxiedUser;
+            this.ldapUser = ldapUser;
+            this.ldapTransaction = ldapTransaction;
+        }
+
+        public UserModel getManagedProxiedUser() {
+            return managedProxiedUser;
+        }
+
+        public LDAPObject getLdapUser() {
+            return ldapUser;
+        }
+
+        public LDAPTransaction getLdapTransaction() {
+            return ldapTransaction;
+        }
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
new file mode 100644
index 0000000..1f2473b
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
@@ -0,0 +1,95 @@
+/*
+ * 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.storage.ldap.mappers;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPTransaction implements KeycloakTransaction {
+
+    public static final Logger logger = Logger.getLogger(LDAPTransaction.class);
+
+    protected TransactionState state = TransactionState.NOT_STARTED;
+
+    private final LDAPStorageProvider ldapProvider;
+    private final LDAPObject ldapUser;
+
+    public LDAPTransaction(LDAPStorageProvider ldapProvider, LDAPObject ldapUser) {
+        this.ldapProvider = ldapProvider;
+        this.ldapUser = ldapUser;
+    }
+
+    @Override
+    public void begin() {
+        if (state != TransactionState.NOT_STARTED) {
+            throw new IllegalStateException("Transaction already started");
+        }
+
+        state = TransactionState.STARTED;
+    }
+
+    @Override
+    public void commit() {
+        if (state != TransactionState.STARTED) {
+            throw new IllegalStateException("Transaction in illegal state for commit: " + state);
+        }
+
+        if (logger.isTraceEnabled()) {
+            logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
+        }
+
+        ldapProvider.getLdapIdentityStore().update(ldapUser);
+        state = TransactionState.FINISHED;
+    }
+
+    @Override
+    public void rollback() {
+        if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
+            throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
+        }
+
+        logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
+        state = TransactionState.FINISHED;
+    }
+
+    @Override
+    public void setRollbackOnly() {
+        state = TransactionState.ROLLBACK_ONLY;
+    }
+
+    @Override
+    public boolean getRollbackOnly() {
+        return state == TransactionState.ROLLBACK_ONLY;
+    }
+
+    @Override
+    public boolean isActive() {
+        return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
+    }
+
+
+    protected enum TransactionState {
+        NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
+    }
+}
+
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
index 4b926bb..2bbe839 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java
@@ -33,6 +33,7 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
 import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
 import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
+import org.keycloak.storage.ldap.mappers.TxAwareLDAPUserModelDelegate;
 
 import javax.naming.AuthenticationException;
 import java.util.HashSet;
@@ -101,7 +102,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
             control.remove(UserAccountControl.ACCOUNTDISABLE);
         }
 
-        updateUserAccountControl(ldapUser, control);
+        updateUserAccountControl(true, ldapUser, control);
     }
 
     @Override
@@ -187,23 +188,26 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
         return new UserAccountControl(longValue);
     }
 
-    // Update user in LDAP
-    protected void updateUserAccountControl(LDAPObject ldapUser, UserAccountControl accountControl) {
+    // Update user in LDAP if "updateInLDAP" is true. Otherwise it is assumed that LDAP update will be called at the end of transaction
+    protected void updateUserAccountControl(boolean updateInLDAP, LDAPObject ldapUser, UserAccountControl accountControl) {
         String userAccountControlValue = String.valueOf(accountControl.getValue());
         logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue);
 
         ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue);
-        ldapProvider.getLdapIdentityStore().update(ldapUser);
+
+        if (updateInLDAP) {
+            ldapProvider.getLdapIdentityStore().update(ldapUser);
+        }
     }
 
 
 
-    public class MSADUserModelDelegate extends UserModelDelegate {
+    public class MSADUserModelDelegate extends TxAwareLDAPUserModelDelegate {
 
         private final LDAPObject ldapUser;
 
         public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
-            super(delegate);
+            super(delegate, ldapProvider, ldapUser);
             this.ldapUser = ldapUser;
         }
 
@@ -235,7 +239,9 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
                     control.add(UserAccountControl.ACCOUNTDISABLE);
                 }
 
-                updateUserAccountControl(ldapUser, control);
+                ensureTransactionStarted();
+
+                updateUserAccountControl(false, ldapUser, control);
             }
         }
 
@@ -257,7 +263,8 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
                 ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
 
                 ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0");
-                ldapProvider.getLdapIdentityStore().update(ldapUser);
+
+                ensureTransactionStarted();
             }
         }
 
@@ -283,7 +290,8 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
                     ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
 
                     ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
-                    ldapProvider.getLdapIdentityStore().update(ldapUser);
+
+                    ensureTransactionStarted();
                 }
             }
         }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java
index 0eac7ae..81566f7 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java
@@ -48,7 +48,7 @@ public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStor
         return ProviderConfigurationBuilder.create()
                 .property().name(MSADUserAccountControlStorageMapper.LDAP_PASSWORD_POLICY_HINTS_ENABLED)
                 .label("Password Policy Hints Enabled")
-                .helpText("Applicable just for writable MSAD. If on, then updating password in MSAD will use LDAP_SERVER_POLICY_HINTS_OID " +
+                .helpText("Applicable just for writable MSAD. If on, then updating password of MSAD user will use LDAP_SERVER_POLICY_HINTS_OID " +
                         "extension, which means that advanced MSAD password policies like 'password history' or 'minimal password age' will be applied. This extension works just for MSAD 2008 R2 or newer.")
                 .type(ProviderConfigProperty.BOOLEAN_TYPE)
                 .defaultValue("false")
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
index 4fc5caf..2bf88f2 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
@@ -18,7 +18,6 @@
 package org.keycloak.storage.ldap.mappers;
 
 import org.jboss.logging.Logger;
-import org.keycloak.models.KeycloakTransaction;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.UserModelDelegate;
 import org.keycloak.storage.ldap.LDAPStorageProvider;
@@ -33,39 +32,16 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
 
     protected LDAPStorageProvider provider;
     protected LDAPObject ldapUser;
-    private final LDAPTransaction transaction;
 
     public TxAwareLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider, LDAPObject ldapUser) {
         super(delegate);
         this.provider = provider;
         this.ldapUser = ldapUser;
-        this.transaction = findOrCreateTransaction();
-    }
-
-    public LDAPTransaction getTransaction() {
-        return transaction;
-    }
-
-    // Try to find transaction in any delegate. We want to enlist just single transaction per all delegates
-    private LDAPTransaction findOrCreateTransaction() {
-        UserModelDelegate delegate = this;
-        while (true) {
-            UserModel deleg = delegate.getDelegate();
-            if (!(deleg instanceof UserModelDelegate)) {
-                return new LDAPTransaction();
-            } else {
-                delegate = (UserModelDelegate) deleg;
-            }
-
-            if (delegate instanceof TxAwareLDAPUserModelDelegate) {
-                TxAwareLDAPUserModelDelegate txDelegate = (TxAwareLDAPUserModelDelegate) delegate;
-                return txDelegate.getTransaction();
-            }
-        }
     }
 
     protected void ensureTransactionStarted() {
-        if (transaction.state == TransactionState.NOT_STARTED) {
+        LDAPTransaction transaction = provider.getUserManager().getTransaction(getId());
+        if (transaction.state == LDAPTransaction.TransactionState.NOT_STARTED) {
             if (logger.isTraceEnabled()) {
                 logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString());
             }
@@ -74,63 +50,4 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
         }
     }
 
-
-
-    protected class LDAPTransaction implements KeycloakTransaction {
-
-        protected TransactionState state = TransactionState.NOT_STARTED;
-
-        @Override
-        public void begin() {
-            if (state != TransactionState.NOT_STARTED) {
-                throw new IllegalStateException("Transaction already started");
-            }
-
-            state = TransactionState.STARTED;
-        }
-
-        @Override
-        public void commit() {
-            if (state != TransactionState.STARTED) {
-                throw new IllegalStateException("Transaction in illegal state for commit: " + state);
-            }
-
-            if (logger.isTraceEnabled()) {
-                logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
-            }
-
-            provider.getLdapIdentityStore().update(ldapUser);
-            state = TransactionState.FINISHED;
-        }
-
-        @Override
-        public void rollback() {
-            if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
-                throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
-            }
-
-            logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
-            state = TransactionState.FINISHED;
-        }
-
-        @Override
-        public void setRollbackOnly() {
-            state = TransactionState.ROLLBACK_ONLY;
-        }
-
-        @Override
-        public boolean getRollbackOnly() {
-            return state == TransactionState.ROLLBACK_ONLY;
-        }
-
-        @Override
-        public boolean isActive() {
-            return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
-        }
-    }
-
-    protected enum TransactionState {
-        NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
-    }
-
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
index 4ea8ed7..1524981 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.testsuite.federation.storage.ldap;
 
+import org.jboss.logging.Logger;
 import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.FixMethodOrder;
@@ -74,6 +75,8 @@ import static org.junit.Assert.assertEquals;
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class LDAPProvidersIntegrationTest {
 
+    private static final Logger log = Logger.getLogger(LDAPProvidersIntegrationTest.class);
+
     private static LDAPRule ldapRule = new LDAPRule();
 
     private static ComponentModel ldapModel = null;
@@ -388,6 +391,10 @@ public class LDAPProvidersIntegrationTest {
             Assert.assertNotNull(user);
             Assert.assertNotNull(user.getFederationLink());
             Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
+            Assert.assertEquals("registerusersuccess2", user.getUsername());
+            Assert.assertEquals("firstName", user.getFirstName());
+            Assert.assertEquals("lastName", user.getLastName());
+            Assert.assertTrue(user.isEnabled());
         } finally {
             keycloakRule.stopSession(session, false);
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADMapperTest.java
new file mode 100644
index 0000000..646e039
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/MSADMapperTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.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.idm.model.LDAPObject;
+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.LoginPasswordUpdatePage;
+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;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class MSADMapperTest {
+
+    // Run this test just on MSAD
+    private static LDAPRule ldapRule = new LDAPRule((Map<String, String> ldapConfig) -> {
+
+        String vendor = ldapConfig.get(LDAPConstants.VENDOR);
+        return !(vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY));
+
+    });
+
+
+    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(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(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);
+
+            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+
+            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;
+
+    @WebResource
+    protected LoginPasswordUpdatePage passwordUpdatePage;
+
+
+    @Test
+    public void test01RegisterUserWithWeakPasswordFirst() {
+        loginPage.open();
+        loginPage.clickRegister();
+        registerPage.assertCurrent();
+
+        // Weak password. This will fail to update password to MSAD due to password policy.
+        registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "password", "password");
+
+        // Another weak password
+        passwordUpdatePage.assertCurrent();
+        passwordUpdatePage.changePassword("pass", "pass");
+        Assert.assertEquals("Invalid password: new password doesn't match password policies.", passwordUpdatePage.getError());
+
+        // Strong password. Successfully update password and being redirected to the app
+        passwordUpdatePage.changePassword("Password1", "Password1");
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("registerUserSuccess2", appRealm);
+            Assert.assertNotNull(user);
+            Assert.assertNotNull(user.getFederationLink());
+            Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
+            Assert.assertEquals("registerusersuccess2", user.getUsername());
+            Assert.assertEquals("firstName", user.getFirstName());
+            Assert.assertEquals("lastName", user.getLastName());
+            Assert.assertTrue(user.isEnabled());
+            Assert.assertEquals(0, user.getRequiredActions().size());
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+}