keycloak-uncached
Changes
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java 11(+6 -5)
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java 25(+16 -9)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java 5(+5 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPOperationDecorator.java 32(+32 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperManager.java 59(+59 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/LDAPServerPolicyHintsDecorator.java 49(+49 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapper.java 36(+28 -8)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/MSADUserAccountControlStorageMapperFactory.java 25(+24 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java 15(+11 -4)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/PasswordUpdateCallback.java 10(+6 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java 4(+2 -2)
Details
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java
index d86a238..c27b2c6 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java
@@ -152,9 +152,9 @@ public class LDAPQuery {
public List<LDAPObject> getResultList() {
// Apply mappers now
- List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersAsc(mappers);
+ List<ComponentModel> sortedMappers = ldapFedProvider.getMapperManager().sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
- LDAPStorageMapper fedMapper = ldapFedProvider.getMapper(mapperModel);
+ LDAPStorageMapper fedMapper = ldapFedProvider.getMapperManager().getMapper(mapperModel);
fedMapper.beforeLDAPQuery(this);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java
index 4b2010b..5a57d28 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java
@@ -20,6 +20,7 @@ package org.keycloak.storage.ldap.idm.store;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import javax.naming.AuthenticationException;
import java.util.List;
@@ -92,7 +93,8 @@ public interface IdentityStore {
*
* @param user Keycloak user
* @param password Ldap password
+ * @param passwordUpdateDecorator Callback to be executed before/after password update. Can be null
*/
- void updatePassword(LDAPObject user, String password);
+ void updatePassword(LDAPObject user, String password, LDAPOperationDecorator passwordUpdateDecorator);
}
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 fec84b2..c0e84b0 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
@@ -29,6 +29,7 @@ import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.IdentityStore;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
@@ -205,7 +206,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
@Override
- public void updatePassword(LDAPObject user, String password) {
+ public void updatePassword(LDAPObject user, String password, LDAPOperationDecorator passwordUpdateDecorator) {
String userDN = user.getDn().toString();
if (logger.isDebugEnabled()) {
@@ -213,7 +214,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
if (getConfig().isActiveDirectory()) {
- updateADPassword(userDN, password);
+ updateADPassword(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
@@ -222,7 +223,7 @@ public class LDAPIdentityStore implements IdentityStore {
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
- operationManager.modifyAttribute(userDN, mod0);
+ operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
@@ -232,7 +233,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
- private void updateADPassword(String userDN, String password) {
+ private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
try {
// Replace the "unicdodePwd" attribute with a new value
// Password must be both Unicode and a quoted string
@@ -244,7 +245,7 @@ public class LDAPIdentityStore implements IdentityStore {
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
- operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
+ operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}), passwordUpdateDecorator);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
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 2f07254..350b16d 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
@@ -22,6 +22,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import javax.naming.AuthenticationException;
import javax.naming.Binding;
@@ -81,7 +82,7 @@ public class LDAPOperationManager {
*/
public void modifyAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
- modifyAttributes(dn, mods);
+ modifyAttributes(dn, mods, null);
}
/**
@@ -101,7 +102,7 @@ public class LDAPOperationManager {
modItems.add(modItem);
}
- modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}));
+ modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}), null);
} catch (NamingException ne) {
throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", ne);
}
@@ -119,7 +120,7 @@ public class LDAPOperationManager {
*/
public void removeAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
- modifyAttributes(dn, mods);
+ modifyAttributes(dn, mods, null);
}
/**
@@ -132,7 +133,7 @@ public class LDAPOperationManager {
*/
public void addAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
- modifyAttributes(dn, mods);
+ modifyAttributes(dn, mods, null);
}
/**
@@ -379,7 +380,7 @@ public class LDAPOperationManager {
}
}
- public void modifyAttributes(final String dn, final ModificationItem[] mods) {
+ public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
try {
if (logger.isTraceEnabled()) {
logger.tracef("Modifying attributes for entry [%s]: [", dn);
@@ -405,7 +406,7 @@ public class LDAPOperationManager {
context.modifyAttributes(dn, mods);
return null;
}
- });
+ }, decorator);
} catch (NamingException e) {
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
}
@@ -546,13 +547,19 @@ public class LDAPOperationManager {
}
private <R> R execute(LdapOperation<R> operation) throws NamingException {
+ return execute(operation, null);
+ }
+
+ private <R> R execute(LdapOperation<R> operation, LDAPOperationDecorator decorator) throws NamingException {
LdapContext context = null;
try {
context = createLdapContext();
+ if (decorator != null) {
+ decorator.beforeLDAPOperation(context, operation);
+ }
+
return operation.execute(context);
- } catch (NamingException ne) {
- throw ne;
} finally {
if (context != null) {
try {
@@ -564,7 +571,7 @@ public class LDAPOperationManager {
}
}
- private interface LdapOperation<R> {
+ public interface LdapOperation<R> {
R execute(LdapContext context) throws NamingException;
}
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 b98d31d..8edb6ca 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
@@ -40,6 +40,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.cache.UserCache;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -49,9 +50,10 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
-import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
-import org.keycloak.storage.ldap.mappers.PasswordUpdated;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapperManager;
+import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
@@ -59,7 +61,6 @@ import org.keycloak.storage.user.UserRegistrationProvider;
import javax.naming.AuthenticationException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -89,7 +90,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
protected LDAPIdentityStore ldapIdentityStore;
protected EditMode editMode;
protected LDAPProviderKerberosConfig kerberosConfig;
- protected PasswordUpdated updater;
+ protected PasswordUpdateCallback updater;
+ protected LDAPStorageMapperManager mapperManager;
protected final Set<String> supportedCredentialTypes = new HashSet<>();
@@ -100,6 +102,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
this.ldapIdentityStore = ldapIdentityStore;
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
this.editMode = ldapIdentityStore.getConfig().getEditMode();
+ this.mapperManager = new LDAPStorageMapperManager(this);
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
if (kerberosConfig.isAllowKerberosAuthentication()) {
@@ -107,7 +110,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
}
- public void setUpdater(PasswordUpdated updater) {
+ public void setUpdater(PasswordUpdateCallback updater) {
this.updater = updater;
}
@@ -127,6 +130,10 @@ public class LDAPStorageProvider implements UserStorageProvider,
return model;
}
+ public LDAPStorageMapperManager getMapperManager() {
+ return mapperManager;
+ }
+
@Override
public UserModel validate(RealmModel realm, UserModel local) {
LDAPObject ldapObject = loadAndValidateUser(realm, local);
@@ -154,9 +161,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = sortMappersAsc(mappers);
+ List<ComponentModel> sortedMappers = mapperManager.sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
- LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
proxied = ldapMapper.proxy(ldapObject, proxied, realm);
}
@@ -299,9 +306,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = sortMappersAsc(mappers);
+ List<ComponentModel> sortedMappers = mapperManager.sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
- LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
List<UserModel> users = ldapMapper.getGroupMembers(realm, group, firstResult, maxResults);
// Sufficient for now
@@ -410,12 +417,12 @@ public class LDAPStorageProvider implements UserStorageProvider,
imported.setEnabled(true);
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = sortMappersDesc(mappers);
+ List<ComponentModel> sortedMappers = mapperManager.sortMappersDesc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
- LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, imported, realm, true);
}
@@ -492,12 +499,12 @@ public class LDAPStorageProvider implements UserStorageProvider,
} catch (AuthenticationException ae) {
boolean processed = false;
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = sortMappersDesc(mappers);
+ List<ComponentModel> sortedMappers = mapperManager.sortMappersDesc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
- LDAPStorageMapper ldapMapper = getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
processed = processed || ldapMapper.onAuthenticationFailure(ldapUser, user, ae, realm);
}
return processed;
@@ -508,23 +515,29 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
- if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof UserCredentialModel)) return false;
+ if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof PasswordUserCredentialModel)) return false;
if (editMode == UserStorageProvider.EditMode.READ_ONLY) {
throw new ModelReadOnlyException("Federated storage is not writable");
} else if (editMode == UserStorageProvider.EditMode.WRITABLE) {
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
- UserCredentialModel cred = (UserCredentialModel)input;
+ PasswordUserCredentialModel cred = (PasswordUserCredentialModel)input;
String password = cred.getValue();
LDAPObject ldapUser = loadAndValidateUser(realm, user);
try {
- ldapIdentityStore.updatePassword(ldapUser, password);
- if (updater != null) updater.passwordUpdated(user, ldapUser, input);
+ LDAPOperationDecorator operationDecorator = null;
+ if (updater != null) {
+ operationDecorator = updater.beforePasswordUpdate(user, ldapUser, cred);
+ }
+
+ ldapIdentityStore.updatePassword(ldapUser, password, operationDecorator);
+
+ if (updater != null) updater.passwordUpdated(user, ldapUser, cred);
return true;
} catch (ModelException me) {
if (updater != null) {
- updater.passwordUpdateFailed(user, ldapUser, input, me);
+ updater.passwordUpdateFailed(user, ldapUser, cred, me);
return false;
} else {
throw me;
@@ -667,23 +680,5 @@ public class LDAPStorageProvider implements UserStorageProvider,
return ldapUser;
}
- public LDAPStorageMapper getMapper(ComponentModel mapperModel) {
- LDAPStorageMapper ldapMapper = getSession().getProvider(LDAPStorageMapper.class, mapperModel);
- if (ldapMapper == null) {
- throw new ModelException("Can't find mapper type with ID: " + mapperModel.getProviderId());
- }
-
- return ldapMapper;
- }
-
-
- public List<ComponentModel> sortMappersAsc(Collection<ComponentModel> mappers) {
- return LDAPMappersComparator.sortAsc(getLdapIdentityStore().getConfig(), mappers);
- }
-
- protected List<ComponentModel> sortMappersDesc(Collection<ComponentModel> mappers) {
- return LDAPMappersComparator.sortDesc(getLdapIdentityStore().getConfig(), mappers);
- }
-
}
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 003d7bf..cd65604 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
@@ -521,9 +521,9 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
// Update keycloak user
List<ComponentModel> federationMappers = currentRealm.getComponents(fedModel.getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersDesc(federationMappers);
+ List<ComponentModel> sortedMappers = ldapFedProvider.getMapperManager().sortMappersDesc(federationMappers);
for (ComponentModel mapperModel : sortedMappers) {
- LDAPStorageMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = ldapFedProvider.getMapperManager().getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, currentUser, currentRealm, false);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
index 87754f5..29f561a 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
@@ -61,9 +61,9 @@ public class LDAPUtils {
ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
List<ComponentModel> federationMappers = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
- List<ComponentModel> sortedMappers = ldapProvider.sortMappersAsc(federationMappers);
+ List<ComponentModel> sortedMappers = ldapProvider.getMapperManager().sortMappersAsc(federationMappers);
for (ComponentModel mapperModel : sortedMappers) {
- LDAPStorageMapper ldapMapper = ldapProvider.getMapper(mapperModel);
+ LDAPStorageMapper ldapMapper = ldapProvider.getMapperManager().getMapper(mapperModel);
ldapMapper.onRegisterUserToLDAP(ldapUser, user, realm);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java
index 12fc079..4b37784 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java
@@ -85,6 +85,11 @@ public class FullNameLDAPStorageMapperFactory extends AbstractLDAPStorageMapperF
}
@Override
+ public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
+ return getConfigProps(parent);
+ }
+
+ @Override
public String getId() {
return PROVIDER_ID;
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPOperationDecorator.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPOperationDecorator.java
new file mode 100644
index 0000000..4815206
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPOperationDecorator.java
@@ -0,0 +1,32 @@
+/*
+ * 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 javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPOperationManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface LDAPOperationDecorator {
+
+ void beforeLDAPOperation(LdapContext ldapContext, LDAPOperationManager.LdapOperation ldapOperation) throws NamingException;
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperManager.java
new file mode 100644
index 0000000..b8e9dc4
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperManager.java
@@ -0,0 +1,59 @@
+/*
+ * 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 java.util.Collection;
+import java.util.List;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+
+/**
+ * TODO: LDAPStorageMapper should be divided into more interfaces and let the LDAPStorageMapperManager to check which operation (feature) is supported by which mapper implementation
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPStorageMapperManager {
+
+ private final LDAPStorageProvider ldapProvider;
+
+ public LDAPStorageMapperManager(LDAPStorageProvider ldapProvider) {
+ this.ldapProvider = ldapProvider;
+ }
+
+ public LDAPStorageMapper getMapper(ComponentModel mapperModel) {
+ LDAPStorageMapper ldapMapper = ldapProvider.getSession().getProvider(LDAPStorageMapper.class, mapperModel);
+ if (ldapMapper == null) {
+ throw new ModelException("Can't find mapper type with ID: " + mapperModel.getProviderId());
+ }
+
+ return ldapMapper;
+ }
+
+
+ public List<ComponentModel> sortMappersAsc(Collection<ComponentModel> mappers) {
+ return LDAPMappersComparator.sortAsc(ldapProvider.getLdapIdentityStore().getConfig(), mappers);
+ }
+
+ public List<ComponentModel> sortMappersDesc(Collection<ComponentModel> mappers) {
+ return LDAPMappersComparator.sortDesc(ldapProvider.getLdapIdentityStore().getConfig(), mappers);
+ }
+
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/LDAPServerPolicyHintsDecorator.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/LDAPServerPolicyHintsDecorator.java
new file mode 100644
index 0000000..97a8f43
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msad/LDAPServerPolicyHintsDecorator.java
@@ -0,0 +1,49 @@
+/*
+ * 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.msad;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.BasicControl;
+import javax.naming.ldap.LdapContext;
+
+import org.jboss.logging.Logger;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPOperationManager;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPServerPolicyHintsDecorator implements LDAPOperationDecorator {
+
+ private static final Logger logger = Logger.getLogger(LDAPServerPolicyHintsDecorator.class);
+
+ public static final String LDAP_SERVER_POLICY_HINTS_OID = "1.2.840.113556.1.4.2239";
+ public static final String LDAP_SERVER_POLICY_HINTS_DEPRECATED_OID = "1.2.840.113556.1.4.2066";
+
+ @Override
+ public void beforeLDAPOperation(LdapContext ldapContext, LDAPOperationManager.LdapOperation ldapOperation) throws NamingException {
+ logger.debug("Applying LDAP_PASSWORD_POLICY_HINTS_OID before update password");
+
+ final byte[] controlData = {48, (byte) 132, 0, 0, 0, 3, 2, 1, 1};
+
+ // Rather using deprecated OID as it works from MSAD 2008-R2 when the newer works from MSAD 2012
+ BasicControl control = new BasicControl(LDAP_SERVER_POLICY_HINTS_DEPRECATED_OID, true, controlData);
+ BasicControl[] controls = new BasicControl[] { control };
+ ldapContext.setRequestControls(controls);
+ }
+}
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 57164e6..4b926bb 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
@@ -24,13 +24,15 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
-import org.keycloak.storage.ldap.mappers.PasswordUpdated;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
+import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
import javax.naming.AuthenticationException;
import java.util.HashSet;
@@ -44,12 +46,14 @@ import java.util.regex.Pattern;
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdated {
+public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdateCallback {
+
+ public static final String LDAP_PASSWORD_POLICY_HINTS_ENABLED = "ldap.password.policy.hints.enabled";
private static final Logger logger = Logger.getLogger(MSADUserAccountControlStorageMapper.class);
private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*");
- private static final Pattern AUTH_INVALID_NEW_PASSWORD = Pattern.compile(".*error code ([0-9a-f]+) .*WILL_NOT_PERFORM.*");
+ private static final Pattern AUTH_INVALID_NEW_PASSWORD = Pattern.compile(".*ERROR CODE ([0-9A-F]+) - ([0-9A-F]+): .*WILL_NOT_PERFORM.*");
public MSADUserAccountControlStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider);
@@ -70,7 +74,18 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
}
@Override
- public void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input) {
+ public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) {
+ // Not apply policies if password is reset by admin (not by user himself)
+ if (password.isAdminRequest()) {
+ return null;
+ }
+
+ boolean applyDecorator = mapperModel.get(LDAP_PASSWORD_POLICY_HINTS_ENABLED, false);
+ return applyDecorator ? new LDAPServerPolicyHintsDecorator() : null;
+ }
+
+ @Override
+ public void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) {
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
// Normally it's read-only
@@ -90,7 +105,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
}
@Override
- public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) {
+ public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) {
throw processFailedPasswordUpdateException(exception);
}
@@ -148,12 +163,17 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
}
String exceptionMessage = e.getCause().getMessage().replace('\n', ' ');
+ logger.debugf("Failed to update password in Active Directory. Exception message: %s", exceptionMessage);
+ exceptionMessage = exceptionMessage.toUpperCase();
+
Matcher m = AUTH_INVALID_NEW_PASSWORD.matcher(exceptionMessage);
if (m.matches()) {
String errorCode = m.group(1);
- if (errorCode.equals("53")) {
- ModelException me = new ModelException("invalidPasswordRegexPatternMessage", e);
- me.setParameters(new Object[]{"passwordConstraintViolation"});
+ String errorCode2 = m.group(2);
+
+ // 52D corresponds to ERROR_PASSWORD_RESTRICTION. See https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
+ if ((errorCode.equals("53")) && errorCode2.endsWith("52D")) {
+ ModelException me = new ModelException("invalidPasswordGenericMessage", e);
return me;
}
}
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 9dc1e5a..0eac7ae 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
@@ -21,9 +21,13 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import java.util.ArrayList;
import java.util.List;
@@ -34,9 +38,23 @@ import java.util.List;
public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
- protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+ protected static final List<ProviderConfigProperty> configProperties;
static {
+ configProperties = getConfigProps(null);
+ }
+
+ private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
+ 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 " +
+ "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")
+ .add()
+ .build();
+
}
@Override
@@ -51,6 +69,11 @@ public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStor
}
@Override
+ public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
+ return getConfigProps(parent);
+ }
+
+ @Override
public String getId() {
return PROVIDER_ID;
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
index 517a3c0..f10ac55 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/msadlds/MSADLDSUserAccountControlStorageMapper.java
@@ -24,13 +24,15 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
-import org.keycloak.storage.ldap.mappers.PasswordUpdated;
+import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
+import org.keycloak.storage.ldap.mappers.PasswordUpdateCallback;
import javax.naming.AuthenticationException;
import java.util.HashSet;
@@ -45,7 +47,7 @@ import java.util.regex.Pattern;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
*/
-public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdated {
+public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdateCallback {
private static final Logger logger = Logger.getLogger(MSADLDSUserAccountControlStorageMapper.class);
@@ -71,7 +73,12 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
}
@Override
- public void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input) {
+ public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) {
+ return null; // Not supported for now. Not sure if LDAP_SERVER_POLICY_HINTS_OID works in MSAD LDS
+ }
+
+ @Override
+ public void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) {
logger.debugf("Going to update pwdLastSet for ldap user '%s' after successful password update", ldapUser.getDn().toString());
// Normally it's read-only
@@ -89,7 +96,7 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
}
@Override
- public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) {
+ public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) {
throw processFailedPasswordUpdateException(exception);
}
diff --git a/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java
new file mode 100644
index 0000000..a688ea3
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java
@@ -0,0 +1,38 @@
+/*
+ * 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.models.credential;
+
+import org.keycloak.models.UserCredentialModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PasswordUserCredentialModel extends UserCredentialModel {
+
+ // True if we have password-update request triggered by admin, not by user himself
+ private static final String ADMIN_REQUEST = "adminRequest";
+
+ public boolean isAdminRequest() {
+ Boolean b = (Boolean) this.notes.get(ADMIN_REQUEST);
+ return b!=null && b;
+ }
+
+ public void setAdminRequest(boolean adminRequest) {
+ this.notes.put(ADMIN_REQUEST, adminRequest);
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
index 4be355d..9b1784c 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
@@ -19,7 +19,10 @@ package org.keycloak.models;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
/**
@@ -43,15 +46,24 @@ public class UserCredentialModel implements CredentialInput {
protected String device;
protected String algorithm;
+ // Additional context informations
+ protected Map<String, Object> notes = new HashMap<>();
+
public UserCredentialModel() {
}
- public static UserCredentialModel password(String password) {
- UserCredentialModel model = new UserCredentialModel();
+ public static PasswordUserCredentialModel password(String password) {
+ return password(password, false);
+ }
+
+ public static PasswordUserCredentialModel password(String password, boolean adminRequest) {
+ PasswordUserCredentialModel model = new PasswordUserCredentialModel();
model.setType(PASSWORD);
model.setValue(password);
+ model.setAdminRequest(adminRequest);
return model;
}
+
public static UserCredentialModel passwordToken(String passwordToken) {
UserCredentialModel model = new UserCredentialModel();
model.setType(PASSWORD_TOKEN);
@@ -136,4 +148,16 @@ public class UserCredentialModel implements CredentialInput {
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
+
+ public void setNote(String key, String value) {
+ this.notes.put(key, value);
+ }
+
+ public void removeNote(String key) {
+ this.notes.remove(key);
+ }
+
+ public Object getNote(String key) {
+ return this.notes.get(key);
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
index 0094f0a..d2851b2 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
@@ -96,7 +96,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
credentials.setValue(password);
UserModel user = context.getUser();
try {
- context.getSession().userCredentialManager().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password")));
+ context.getSession().userCredentialManager().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password"), false));
} catch (Exception me) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
index a2c43f7..aa5bf25 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -108,7 +108,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
}
try {
- context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew));
+ context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew, false));
context.success();
} catch (ModelException me) {
errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index cb41012..d754249 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -650,7 +650,7 @@ public class AccountService extends AbstractSecuredLocalService {
}
try {
- session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew));
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew, false));
} catch (ModelReadOnlyException mre) {
setReferrerOnPage();
errorEvent.error(Errors.NOT_ALLOWED);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 83265cb..6e89650 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -49,6 +49,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -776,7 +777,7 @@ public class UsersResource {
throw new BadRequestException("Empty password not allowed");
}
- UserCredentialModel cred = RepresentationToModel.convertCredential(pass);
+ UserCredentialModel cred = UserCredentialModel.password(pass.getValue(), true);
try {
session.userCredentialManager().updateCredential(realm, user, cred);
} catch (IllegalStateException ise) {
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 fdb73d7..4ea8ed7 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
@@ -715,7 +715,7 @@ public class LDAPProvidersIntegrationTest {
}
try {
- UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1");
+ UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1", true);
session.userCredentialManager().updateCredential(appRealm, user, cred);
Assert.fail("should fail");
} catch (ModelReadOnlyException e) {
@@ -856,7 +856,7 @@ public class LDAPProvidersIntegrationTest {
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
- UserCredentialModel cred = UserCredentialModel.password("Candycand1");
+ UserCredentialModel cred = UserCredentialModel.password("Candycand1", true);
session.userCredentialManager().updateCredential(appRealm, user, cred);
CredentialModel userCredentialValueModel = session.userCredentialManager().getStoredCredentialsByType(appRealm, user, CredentialModel.PASSWORD).get(0);
Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
index 8338c48..941d2e9 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
@@ -128,7 +128,7 @@ public class LDAPTestUtils {
}
public static void updateLDAPPassword(LDAPStorageProvider ldapProvider, LDAPObject ldapUser, String password) {
- ldapProvider.getLdapIdentityStore().updatePassword(ldapUser, password);
+ ldapProvider.getLdapIdentityStore().updatePassword(ldapUser, password, null);
// Enable MSAD user through userAccountControls
if (ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 1f42008..90b10de 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -150,6 +150,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
+invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
locale_ca=Catal\u00E0
locale_de=Deutsch
diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
index e734c06..82db91e 100644
--- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
@@ -6,6 +6,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
+invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")".
ldapErrorConnectionTimeoutNotNumber=Connection Timeout must be a number
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 3fc5bc4..823c4af 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -168,6 +168,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
+invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required