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 8edb6ca888..7e112b1e5e 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 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 0000000000..5155c12606 --- /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 Marek Posolda + */ +public class LDAPStorageUserManager { + + private final Map 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 0000000000..1f2473baab --- /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 Marek Posolda + */ +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/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java index 4fc5cafb25..2bf88f23dd 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/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 4b926bb4be..2bbe839b39 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 0eac7ae42f..81566f7765 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/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 4ea8ed765c..152498104f 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 0000000000..646e039a07 --- /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 Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class MSADMapperTest { + + // Run this test just on MSAD + private static LDAPRule ldapRule = new LDAPRule((Map 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 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); + } + } +}