KEYCLOAK-4052 - add an option to validate Password Policy for ldap user storage
This commit is contained in:
parent
1e57f277b8
commit
656fc5d7c0
9 changed files with 66 additions and 9 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -142,6 +142,10 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
|||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue("1")
|
||||
.add()
|
||||
.property().name(LDAPConstants.VALIDATE_PASSWORD_POLICY)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false")
|
||||
.add()
|
||||
.property().name(LDAPConstants.USE_TRUSTSTORE_SPI)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue("ldapsOnly")
|
||||
|
|
|
@ -63,6 +63,8 @@ public class LDAPConstants {
|
|||
|
||||
public static final String EDIT_MODE = "editMode";
|
||||
|
||||
public static final String VALIDATE_PASSWORD_POLICY = "validatePasswordPolicy";
|
||||
|
||||
// Count of users processed per single transaction during sync process
|
||||
public static final String BATCH_SIZE_FOR_SYNC = "batchSizeForSync";
|
||||
public static final int DEFAULT_BATCH_SIZE_FOR_SYNC = 1000;
|
||||
|
|
|
@ -436,6 +436,14 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
|||
testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider);
|
||||
}
|
||||
|
||||
protected void updateProviderValidatePasswordPolicy(Boolean validatePasswordPolicy) {
|
||||
List<ComponentRepresentation> 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");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -173,6 +173,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'ldap.search-scope.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="validatePasswordPolicy">{{:: 'validate-password-policy' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="instance.config['validatePasswordPolicy'][0]" name="validatePasswordPolicy" id="validatePasswordPolicy" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'ldap.validate-password-policy.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="useTruststoreSpi">{{:: 'use-truststore-spi' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
|
@ -469,4 +476,4 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
||||
<kc-menu></kc-menu>
|
||||
|
|
Loading…
Reference in a new issue