Merge pull request #3771 from mposolda/msad-ldap
KEYCLOAK-2333 LDAP/MSAD password policies are not used when user chan…
This commit is contained in:
commit
ef46070410
26 changed files with 351 additions and 83 deletions
|
@ -40,6 +40,7 @@ import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserManager;
|
import org.keycloak.models.UserManager;
|
||||||
import org.keycloak.models.cache.UserCache;
|
import org.keycloak.models.cache.UserCache;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.storage.StorageId;
|
import org.keycloak.storage.StorageId;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
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.query.internal.LDAPQueryConditionsBuilder;
|
||||||
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
|
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||||
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
|
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.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.ImportedUserValidation;
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
|
@ -59,7 +61,6 @@ import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
|
||||||
import javax.naming.AuthenticationException;
|
import javax.naming.AuthenticationException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -89,7 +90,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
protected LDAPIdentityStore ldapIdentityStore;
|
protected LDAPIdentityStore ldapIdentityStore;
|
||||||
protected EditMode editMode;
|
protected EditMode editMode;
|
||||||
protected LDAPProviderKerberosConfig kerberosConfig;
|
protected LDAPProviderKerberosConfig kerberosConfig;
|
||||||
protected PasswordUpdated updater;
|
protected PasswordUpdateCallback updater;
|
||||||
|
protected LDAPStorageMapperManager mapperManager;
|
||||||
|
|
||||||
protected final Set<String> supportedCredentialTypes = new HashSet<>();
|
protected final Set<String> supportedCredentialTypes = new HashSet<>();
|
||||||
|
|
||||||
|
@ -100,6 +102,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
this.ldapIdentityStore = ldapIdentityStore;
|
this.ldapIdentityStore = ldapIdentityStore;
|
||||||
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
|
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
|
||||||
this.editMode = ldapIdentityStore.getConfig().getEditMode();
|
this.editMode = ldapIdentityStore.getConfig().getEditMode();
|
||||||
|
this.mapperManager = new LDAPStorageMapperManager(this);
|
||||||
|
|
||||||
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
|
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
|
||||||
if (kerberosConfig.isAllowKerberosAuthentication()) {
|
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;
|
this.updater = updater;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +130,10 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LDAPStorageMapperManager getMapperManager() {
|
||||||
|
return mapperManager;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserModel validate(RealmModel realm, UserModel local) {
|
public UserModel validate(RealmModel realm, UserModel local) {
|
||||||
LDAPObject ldapObject = loadAndValidateUser(realm, 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> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
|
||||||
List<ComponentModel> sortedMappers = sortMappersAsc(mappers);
|
List<ComponentModel> sortedMappers = mapperManager.sortMappersAsc(mappers);
|
||||||
for (ComponentModel mapperModel : sortedMappers) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
|
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
|
||||||
proxied = ldapMapper.proxy(ldapObject, proxied, realm);
|
proxied = ldapMapper.proxy(ldapObject, proxied, realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,9 +306,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
@Override
|
@Override
|
||||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
|
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) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
|
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
|
||||||
List<UserModel> users = ldapMapper.getGroupMembers(realm, group, firstResult, maxResults);
|
List<UserModel> users = ldapMapper.getGroupMembers(realm, group, firstResult, maxResults);
|
||||||
|
|
||||||
// Sufficient for now
|
// Sufficient for now
|
||||||
|
@ -410,12 +417,12 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
imported.setEnabled(true);
|
imported.setEnabled(true);
|
||||||
|
|
||||||
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
|
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) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
|
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);
|
ldapMapper.onImportUserFromLDAP(ldapUser, imported, realm, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,12 +499,12 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
} catch (AuthenticationException ae) {
|
} catch (AuthenticationException ae) {
|
||||||
boolean processed = false;
|
boolean processed = false;
|
||||||
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
|
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) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
|
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);
|
processed = processed || ldapMapper.onAuthenticationFailure(ldapUser, user, ae, realm);
|
||||||
}
|
}
|
||||||
return processed;
|
return processed;
|
||||||
|
@ -508,23 +515,29 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
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) {
|
if (editMode == UserStorageProvider.EditMode.READ_ONLY) {
|
||||||
throw new ModelReadOnlyException("Federated storage is not writable");
|
throw new ModelReadOnlyException("Federated storage is not writable");
|
||||||
|
|
||||||
} else if (editMode == UserStorageProvider.EditMode.WRITABLE) {
|
} else if (editMode == UserStorageProvider.EditMode.WRITABLE) {
|
||||||
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
|
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
|
||||||
UserCredentialModel cred = (UserCredentialModel)input;
|
PasswordUserCredentialModel cred = (PasswordUserCredentialModel)input;
|
||||||
String password = cred.getValue();
|
String password = cred.getValue();
|
||||||
LDAPObject ldapUser = loadAndValidateUser(realm, user);
|
LDAPObject ldapUser = loadAndValidateUser(realm, user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ldapIdentityStore.updatePassword(ldapUser, password);
|
LDAPOperationDecorator operationDecorator = null;
|
||||||
if (updater != null) updater.passwordUpdated(user, ldapUser, input);
|
if (updater != null) {
|
||||||
|
operationDecorator = updater.beforePasswordUpdate(user, ldapUser, cred);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapIdentityStore.updatePassword(ldapUser, password, operationDecorator);
|
||||||
|
|
||||||
|
if (updater != null) updater.passwordUpdated(user, ldapUser, cred);
|
||||||
return true;
|
return true;
|
||||||
} catch (ModelException me) {
|
} catch (ModelException me) {
|
||||||
if (updater != null) {
|
if (updater != null) {
|
||||||
updater.passwordUpdateFailed(user, ldapUser, input, me);
|
updater.passwordUpdateFailed(user, ldapUser, cred, me);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
throw me;
|
throw me;
|
||||||
|
@ -667,23 +680,5 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
return ldapUser;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -521,9 +521,9 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||||
|
|
||||||
// Update keycloak user
|
// Update keycloak user
|
||||||
List<ComponentModel> federationMappers = currentRealm.getComponents(fedModel.getId(), LDAPStorageMapper.class.getName());
|
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) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
LDAPStorageMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
|
LDAPStorageMapper ldapMapper = ldapFedProvider.getMapperManager().getMapper(mapperModel);
|
||||||
ldapMapper.onImportUserFromLDAP(ldapUser, currentUser, currentRealm, false);
|
ldapMapper.onImportUserFromLDAP(ldapUser, currentUser, currentRealm, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,9 +61,9 @@ public class LDAPUtils {
|
||||||
ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
|
ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
|
||||||
|
|
||||||
List<ComponentModel> federationMappers = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
|
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) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
LDAPStorageMapper ldapMapper = ldapProvider.getMapper(mapperModel);
|
LDAPStorageMapper ldapMapper = ldapProvider.getMapperManager().getMapper(mapperModel);
|
||||||
ldapMapper.onRegisterUserToLDAP(ldapUser, user, realm);
|
ldapMapper.onRegisterUserToLDAP(ldapUser, user, realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -152,9 +152,9 @@ public class LDAPQuery {
|
||||||
public List<LDAPObject> getResultList() {
|
public List<LDAPObject> getResultList() {
|
||||||
|
|
||||||
// Apply mappers now
|
// Apply mappers now
|
||||||
List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersAsc(mappers);
|
List<ComponentModel> sortedMappers = ldapFedProvider.getMapperManager().sortMappersAsc(mappers);
|
||||||
for (ComponentModel mapperModel : sortedMappers) {
|
for (ComponentModel mapperModel : sortedMappers) {
|
||||||
LDAPStorageMapper fedMapper = ldapFedProvider.getMapper(mapperModel);
|
LDAPStorageMapper fedMapper = ldapFedProvider.getMapperManager().getMapper(mapperModel);
|
||||||
fedMapper.beforeLDAPQuery(this);
|
fedMapper.beforeLDAPQuery(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.storage.ldap.idm.store;
|
||||||
import org.keycloak.storage.ldap.LDAPConfig;
|
import org.keycloak.storage.ldap.LDAPConfig;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||||
|
|
||||||
import javax.naming.AuthenticationException;
|
import javax.naming.AuthenticationException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -92,7 +93,8 @@ public interface IdentityStore {
|
||||||
*
|
*
|
||||||
* @param user Keycloak user
|
* @param user Keycloak user
|
||||||
* @param password Ldap password
|
* @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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.EqualCondition;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.storage.ldap.idm.store.IdentityStore;
|
import org.keycloak.storage.ldap.idm.store.IdentityStore;
|
||||||
|
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||||
|
|
||||||
import javax.naming.AuthenticationException;
|
import javax.naming.AuthenticationException;
|
||||||
import javax.naming.NamingEnumeration;
|
import javax.naming.NamingEnumeration;
|
||||||
|
@ -205,7 +206,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updatePassword(LDAPObject user, String password) {
|
public void updatePassword(LDAPObject user, String password, LDAPOperationDecorator passwordUpdateDecorator) {
|
||||||
String userDN = user.getDn().toString();
|
String userDN = user.getDn().toString();
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
@ -213,7 +214,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getConfig().isActiveDirectory()) {
|
if (getConfig().isActiveDirectory()) {
|
||||||
updateADPassword(userDN, password);
|
updateADPassword(userDN, password, passwordUpdateDecorator);
|
||||||
} else {
|
} else {
|
||||||
ModificationItem[] mods = new ModificationItem[1];
|
ModificationItem[] mods = new ModificationItem[1];
|
||||||
|
|
||||||
|
@ -222,7 +223,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
|
|
||||||
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
||||||
|
|
||||||
operationManager.modifyAttribute(userDN, mod0);
|
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
|
||||||
} catch (ModelException me) {
|
} catch (ModelException me) {
|
||||||
throw me;
|
throw me;
|
||||||
} catch (Exception e) {
|
} 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 {
|
try {
|
||||||
// Replace the "unicdodePwd" attribute with a new value
|
// Replace the "unicdodePwd" attribute with a new value
|
||||||
// Password must be both Unicode and a quoted string
|
// Password must be both Unicode and a quoted string
|
||||||
|
@ -244,7 +245,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
|
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
|
||||||
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
|
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) {
|
} catch (ModelException me) {
|
||||||
throw me;
|
throw me;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.storage.ldap.LDAPConfig;
|
import org.keycloak.storage.ldap.LDAPConfig;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||||
|
|
||||||
import javax.naming.AuthenticationException;
|
import javax.naming.AuthenticationException;
|
||||||
import javax.naming.Binding;
|
import javax.naming.Binding;
|
||||||
|
@ -81,7 +82,7 @@ public class LDAPOperationManager {
|
||||||
*/
|
*/
|
||||||
public void modifyAttribute(String dn, Attribute attribute) {
|
public void modifyAttribute(String dn, Attribute attribute) {
|
||||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_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);
|
modItems.add(modItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}));
|
modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}), null);
|
||||||
} catch (NamingException ne) {
|
} catch (NamingException ne) {
|
||||||
throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", 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) {
|
public void removeAttribute(String dn, Attribute attribute) {
|
||||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_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) {
|
public void addAttribute(String dn, Attribute attribute) {
|
||||||
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_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 {
|
try {
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
||||||
|
@ -405,7 +406,7 @@ public class LDAPOperationManager {
|
||||||
context.modifyAttributes(dn, mods);
|
context.modifyAttributes(dn, mods);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
}, decorator);
|
||||||
} catch (NamingException e) {
|
} catch (NamingException e) {
|
||||||
throw new ModelException("Could not modify attribute for DN [" + dn + "]", 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 {
|
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;
|
LdapContext context = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context = createLdapContext();
|
context = createLdapContext();
|
||||||
|
if (decorator != null) {
|
||||||
|
decorator.beforeLDAPOperation(context, operation);
|
||||||
|
}
|
||||||
|
|
||||||
return operation.execute(context);
|
return operation.execute(context);
|
||||||
} catch (NamingException ne) {
|
|
||||||
throw ne;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -564,7 +571,7 @@ public class LDAPOperationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface LdapOperation<R> {
|
public interface LdapOperation<R> {
|
||||||
R execute(LdapContext context) throws NamingException;
|
R execute(LdapContext context) throws NamingException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,11 @@ public class FullNameLDAPStorageMapperFactory extends AbstractLDAPStorageMapperF
|
||||||
return configProperties;
|
return configProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||||
|
return getConfigProps(parent);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -16,18 +16,20 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.storage.ldap.mappers;
|
package org.keycloak.storage.ldap.mappers;
|
||||||
|
|
||||||
import org.keycloak.credential.CredentialInput;
|
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface PasswordUpdated {
|
public interface PasswordUpdateCallback {
|
||||||
|
|
||||||
void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input);
|
LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password);
|
||||||
|
|
||||||
void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, CredentialInput input, ModelException exception) throws ModelException;
|
void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password);
|
||||||
|
|
||||||
|
void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) throws ModelException;
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,13 +24,15 @@ import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.models.utils.UserModelDelegate;
|
import org.keycloak.models.utils.UserModelDelegate;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
|
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 javax.naming.AuthenticationException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -44,12 +46,14 @@ import java.util.regex.Pattern;
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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 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_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) {
|
public MSADUserAccountControlStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
||||||
super(mapperModel, ldapProvider);
|
super(mapperModel, ldapProvider);
|
||||||
|
@ -70,7 +74,18 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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());
|
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
|
||||||
|
|
||||||
// Normally it's read-only
|
// Normally it's read-only
|
||||||
|
@ -90,7 +105,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
throw processFailedPasswordUpdateException(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,12 +163,17 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
||||||
}
|
}
|
||||||
|
|
||||||
String exceptionMessage = e.getCause().getMessage().replace('\n', ' ');
|
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);
|
Matcher m = AUTH_INVALID_NEW_PASSWORD.matcher(exceptionMessage);
|
||||||
if (m.matches()) {
|
if (m.matches()) {
|
||||||
String errorCode = m.group(1);
|
String errorCode = m.group(1);
|
||||||
if (errorCode.equals("53")) {
|
String errorCode2 = m.group(2);
|
||||||
ModelException me = new ModelException("invalidPasswordRegexPatternMessage", e);
|
|
||||||
me.setParameters(new Object[]{"passwordConstraintViolation"});
|
// 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;
|
return me;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,13 @@ import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
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.LDAPStorageProvider;
|
||||||
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
|
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
|
||||||
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
|
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
|
||||||
|
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -34,9 +38,23 @@ import java.util.List;
|
||||||
public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
|
public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
|
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 {
|
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
|
@Override
|
||||||
|
@ -50,6 +68,11 @@ public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStor
|
||||||
return configProperties;
|
return configProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||||
|
return getConfigProps(parent);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
|
|
@ -24,13 +24,15 @@ import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.models.utils.UserModelDelegate;
|
import org.keycloak.models.utils.UserModelDelegate;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
|
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 javax.naming.AuthenticationException;
|
||||||
import java.util.HashSet;
|
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:mposolda@redhat.com">Marek Posolda</a>
|
||||||
* @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</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);
|
private static final Logger logger = Logger.getLogger(MSADLDSUserAccountControlStorageMapper.class);
|
||||||
|
|
||||||
|
@ -71,7 +73,12 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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());
|
logger.debugf("Going to update pwdLastSet for ldap user '%s' after successful password update", ldapUser.getDn().toString());
|
||||||
|
|
||||||
// Normally it's read-only
|
// Normally it's read-only
|
||||||
|
@ -89,7 +96,7 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
throw processFailedPasswordUpdateException(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,10 @@ package org.keycloak.models;
|
||||||
|
|
||||||
import org.keycloak.credential.CredentialInput;
|
import org.keycloak.credential.CredentialInput;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,15 +46,24 @@ public class UserCredentialModel implements CredentialInput {
|
||||||
protected String device;
|
protected String device;
|
||||||
protected String algorithm;
|
protected String algorithm;
|
||||||
|
|
||||||
|
// Additional context informations
|
||||||
|
protected Map<String, Object> notes = new HashMap<>();
|
||||||
|
|
||||||
public UserCredentialModel() {
|
public UserCredentialModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserCredentialModel password(String password) {
|
public static PasswordUserCredentialModel password(String password) {
|
||||||
UserCredentialModel model = new UserCredentialModel();
|
return password(password, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PasswordUserCredentialModel password(String password, boolean adminRequest) {
|
||||||
|
PasswordUserCredentialModel model = new PasswordUserCredentialModel();
|
||||||
model.setType(PASSWORD);
|
model.setType(PASSWORD);
|
||||||
model.setValue(password);
|
model.setValue(password);
|
||||||
|
model.setAdminRequest(adminRequest);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserCredentialModel passwordToken(String passwordToken) {
|
public static UserCredentialModel passwordToken(String passwordToken) {
|
||||||
UserCredentialModel model = new UserCredentialModel();
|
UserCredentialModel model = new UserCredentialModel();
|
||||||
model.setType(PASSWORD_TOKEN);
|
model.setType(PASSWORD_TOKEN);
|
||||||
|
@ -136,4 +148,16 @@ public class UserCredentialModel implements CredentialInput {
|
||||||
public void setAlgorithm(String algorithm) {
|
public void setAlgorithm(String algorithm) {
|
||||||
this.algorithm = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,7 +96,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
|
||||||
credentials.setValue(password);
|
credentials.setValue(password);
|
||||||
UserModel user = context.getUser();
|
UserModel user = context.getUser();
|
||||||
try {
|
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) {
|
} catch (Exception me) {
|
||||||
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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();
|
context.success();
|
||||||
} catch (ModelException me) {
|
} catch (ModelException me) {
|
||||||
errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
|
errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
|
||||||
|
|
|
@ -650,7 +650,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew));
|
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew, false));
|
||||||
} catch (ModelReadOnlyException mre) {
|
} catch (ModelReadOnlyException mre) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
errorEvent.error(Errors.NOT_ALLOWED);
|
errorEvent.error(Errors.NOT_ALLOWED);
|
||||||
|
|
|
@ -49,6 +49,7 @@ import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserLoginFailureModel;
|
import org.keycloak.models.UserLoginFailureModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
@ -776,7 +777,7 @@ public class UsersResource {
|
||||||
throw new BadRequestException("Empty password not allowed");
|
throw new BadRequestException("Empty password not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
UserCredentialModel cred = RepresentationToModel.convertCredential(pass);
|
UserCredentialModel cred = UserCredentialModel.password(pass.getValue(), true);
|
||||||
try {
|
try {
|
||||||
session.userCredentialManager().updateCredential(realm, user, cred);
|
session.userCredentialManager().updateCredential(realm, user, cred);
|
||||||
} catch (IllegalStateException ise) {
|
} catch (IllegalStateException ise) {
|
||||||
|
|
|
@ -715,7 +715,7 @@ public class LDAPProvidersIntegrationTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1");
|
UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1", true);
|
||||||
session.userCredentialManager().updateCredential(appRealm, user, cred);
|
session.userCredentialManager().updateCredential(appRealm, user, cred);
|
||||||
Assert.fail("should fail");
|
Assert.fail("should fail");
|
||||||
} catch (ModelReadOnlyException e) {
|
} catch (ModelReadOnlyException e) {
|
||||||
|
@ -856,7 +856,7 @@ public class LDAPProvidersIntegrationTest {
|
||||||
Assert.assertNotNull(user.getFederationLink());
|
Assert.assertNotNull(user.getFederationLink());
|
||||||
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
|
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
|
||||||
|
|
||||||
UserCredentialModel cred = UserCredentialModel.password("Candycand1");
|
UserCredentialModel cred = UserCredentialModel.password("Candycand1", true);
|
||||||
session.userCredentialManager().updateCredential(appRealm, user, cred);
|
session.userCredentialManager().updateCredential(appRealm, user, cred);
|
||||||
CredentialModel userCredentialValueModel = session.userCredentialManager().getStoredCredentialsByType(appRealm, user, CredentialModel.PASSWORD).get(0);
|
CredentialModel userCredentialValueModel = session.userCredentialManager().getStoredCredentialsByType(appRealm, user, CredentialModel.PASSWORD).get(0);
|
||||||
Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType());
|
Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType());
|
||||||
|
|
|
@ -128,7 +128,7 @@ public class LDAPTestUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateLDAPPassword(LDAPStorageProvider ldapProvider, LDAPObject ldapUser, String password) {
|
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
|
// Enable MSAD user through userAccountControls
|
||||||
if (ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
if (ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||||
|
|
|
@ -150,6 +150,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
|
||||||
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
||||||
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
||||||
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
|
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_ca=Catal\u00E0
|
||||||
locale_de=Deutsch
|
locale_de=Deutsch
|
||||||
|
|
|
@ -6,6 +6,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
|
||||||
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
||||||
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
||||||
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
|
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 ")".
|
ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")".
|
||||||
ldapErrorConnectionTimeoutNotNumber=Connection Timeout must be a number
|
ldapErrorConnectionTimeoutNotNumber=Connection Timeout must be a number
|
||||||
|
|
|
@ -168,6 +168,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
|
||||||
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
||||||
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
||||||
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
|
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
|
failedToProcessResponseMessage=Failed to process response
|
||||||
httpsRequiredMessage=HTTPS required
|
httpsRequiredMessage=HTTPS required
|
||||||
|
|
Loading…
Reference in a new issue