From 656fc5d7c0f66aa9aa78a8be28a77da50678ec0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Couralet?= Date: Thu, 12 Oct 2017 18:24:23 +0200 Subject: [PATCH] KEYCLOAK-4052 - add an option to validate Password Policy for ldap user storage --- .../org/keycloak/storage/ldap/LDAPConfig.java | 7 ++++- .../storage/ldap/LDAPStorageProvider.java | 10 +++++-- .../ldap/LDAPStorageProviderFactory.java | 4 +++ .../org/keycloak/models/LDAPConstants.java | 2 ++ .../kerberos/AbstractKerberosTest.java | 8 ++++++ .../federation/kerberos/KerberosLdapTest.java | 26 ++++++++++++++++++- .../resources/kerberos/kerberosrealm.json | 5 ++-- .../messages/admin-messages_en.properties | 4 +-- .../resources/partials/user-storage-ldap.html | 9 ++++++- 9 files changed, 66 insertions(+), 9 deletions(-) diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java index d6640357b4..c2b3eb2620 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java @@ -105,6 +105,11 @@ public class LDAPConfig { return vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY); } + public boolean isValidatePasswordPolicy() { + String validatePPolicy = config.getFirst(LDAPConstants.VALIDATE_PASSWORD_POLICY); + return Boolean.parseBoolean(validatePPolicy); + } + public String getConnectionPooling() { return config.getFirst(LDAPConstants.CONNECTION_POOLING); } @@ -137,7 +142,7 @@ public class LDAPConfig { return uuidAttrName; } - + public boolean isObjectGUID() { return getUuidLDAPAttributeName().equalsIgnoreCase(LDAPConstants.OBJECT_GUID); } 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 e77df53622..69c513d0c1 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 @@ -44,6 +44,9 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; +import org.keycloak.models.utils.ReadOnlyUserModelDelegate; +import org.keycloak.policy.PasswordPolicyManagerProvider; +import org.keycloak.policy.PolicyError; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -533,7 +536,7 @@ public class LDAPStorageProvider implements UserStorageProvider, // Check here if user already exists String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig()); UserModel user = session.userLocalStorage().getUserByUsername(ldapUsername, realm); - + if (user != null) { LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig()); // If email attribute mapper is set to "Always Read Value From LDAP" the user may be in Keycloak DB with an old email address @@ -599,7 +602,10 @@ public class LDAPStorageProvider implements UserStorageProvider, PasswordUserCredentialModel cred = (PasswordUserCredentialModel)input; String password = cred.getValue(); LDAPObject ldapUser = loadAndValidateUser(realm, user); - + if (ldapIdentityStore.getConfig().isValidatePasswordPolicy()) { + PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, password); + if (error != null) throw new ModelException(error.getMessage(), error.getParameters()); + } try { LDAPOperationDecorator operationDecorator = null; if (updater != null) { diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java index 0d4c07bdd5..77029c0005 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java @@ -142,6 +142,10 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory reps = testRealmResource().components().query("test", UserStorageProvider.class.getName()); + Assert.assertEquals(1, reps.size()); + ComponentRepresentation kerberosProvider = reps.get(0); + kerberosProvider.getConfig().putSingle(LDAPConstants.VALIDATE_PASSWORD_POLICY, validatePasswordPolicy.toString()); + testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider); + } + public RealmResource testRealmResource() { return adminClient.realm("test"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java index 5d5eb73d84..3cdddc6602 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java @@ -17,19 +17,24 @@ package org.keycloak.testsuite.federation.kerberos; +import java.io.File; import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; +import org.apache.commons.io.FileUtils; +import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.models.PasswordPolicy; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; @@ -75,7 +80,7 @@ public class KerberosLdapTest extends AbstractKerberosTest { protected boolean isCaseSensitiveLogin() { return kerberosRule.isCaseSensitiveLogin(); } - + @Override protected boolean isStartEmbeddedLdapServer() { return kerberosRule.isStartEmbeddedLdapServer(); @@ -96,6 +101,25 @@ public class KerberosLdapTest extends AbstractKerberosTest { assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false); } + @Test + public void validatePasswordPolicyTest() throws Exception{ + updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE); + + changePasswordPage.open(); + loginPage.login("jduke", "theduke"); + + updateProviderValidatePasswordPolicy(true); + changePasswordPage.changePassword("theduke", "jduke", "jduke"); + Assert.assertTrue(driver.getPageSource().contains("Invalid")); + + updateProviderValidatePasswordPolicy(false); + changePasswordPage.changePassword("theduke", "jduke", "jduke"); + Assert.assertTrue(driver.getPageSource().contains("Your password has been updated.")); + + // Change password back + changePasswordPage.open(); + changePasswordPage.changePassword("jduke", "theduke", "theduke"); + } @Test public void writableEditModeTest() throws Exception { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json index b48faf1d6e..2588e4aab3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json @@ -8,6 +8,7 @@ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password", "kerberos" ], + "passwordPolicy": "notUsername(undefined)", "defaultRoles": [ "user" ], "users" : [ { @@ -42,7 +43,7 @@ ], "secret": "password" } - ], + ], "roles" : { "realm" : [ { @@ -52,4 +53,4 @@ ] }, "eventsListeners": ["jboss-logging", "event-queue"] -} \ No newline at end of file +} diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 3fa92bb3a1..09a8c3fe47 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -808,11 +808,13 @@ search-scope=Search Scope ldap.search-scope.tooltip=For one level, we search for users just in DNs specified by User DNs. For subtree, we search in whole of their subtree. See LDAP documentation for more details use-truststore-spi=Use Truststore SPI ldap.use-truststore-spi.tooltip=Specifies whether LDAP connection will use the truststore SPI with the truststore configured in standalone.xml/domain.xml. 'Always' means that it will always use it. 'Never' means that it won't use it. 'Only for ldaps' means that it will use if your connection URL use ldaps. Note even if standalone.xml/domain.xml is not configured, the default Java cacerts or certificate specified by 'javax.net.ssl.trustStore' property will be used. +validate-password-policy=Validate Password Policy connection-pooling=Connection Pooling ldap-connection-timeout=Connection Timeout ldap.connection-timeout.tooltip=LDAP Connection Timeout in milliseconds ldap-read-timeout=Read Timeout ldap.read-timeout.tooltip=LDAP Read Timeout in milliseconds. This timeout applies for LDAP read operations +ldap.validate-password-policy.tooltip=Does Keycloak should validate the password with the realm password policy before updating it ldap.connection-pooling.tooltip=Does Keycloak should use connection pooling for accessing LDAP server ldap.pagination.tooltip=Does the LDAP server support pagination. kerberos-integration=Kerberos Integration @@ -1367,5 +1369,3 @@ map-roles-authz-users-scope-description=Policies that decide if admin can map ro user-impersonated-authz-users-scope-description=Policies that decide which users can be impersonated. These policies are applied to the user being impersonated. manage-membership-authz-group-scope-description=Policies that decide if admin can add or remove users from this group manage-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group - - diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html index 321d047187..947345060d 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html @@ -173,6 +173,13 @@ {{:: 'ldap.search-scope.tooltip' | translate}} +
+ +
+ +
+ {{:: 'ldap.validate-password-policy.tooltip' | translate}} +
@@ -469,4 +476,4 @@
- \ No newline at end of file +