Merge pull request #1976 from mposolda/master
KEYCLOAK-2178 KEYCLOAK-1744 Added MSADUserAccountControlMapper. Remov…
This commit is contained in:
commit
d3247bc66a
23 changed files with 516 additions and 54 deletions
|
@ -113,12 +113,6 @@ public class LDAPConfig {
|
|||
return uuidAttrName;
|
||||
}
|
||||
|
||||
// TODO: Remove and use mapper instead?
|
||||
public boolean isUserAccountControlsAfterPasswordUpdate() {
|
||||
String userAccountCtrls = config.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE);
|
||||
return userAccountCtrls==null ? false : Boolean.parseBoolean(userAccountCtrls);
|
||||
}
|
||||
|
||||
public boolean isPagination() {
|
||||
String pagination = config.get(LDAPConstants.PAGINATION);
|
||||
return pagination==null ? false : Boolean.parseBoolean(pagination);
|
||||
|
|
|
@ -41,6 +41,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -366,7 +368,22 @@ public class LDAPFederationProvider implements UserFederationProvider {
|
|||
} else {
|
||||
// Use Naming LDAP API
|
||||
LDAPObject ldapUser = loadAndValidateUser(realm, user);
|
||||
return ldapIdentityStore.validatePassword(ldapUser, password);
|
||||
|
||||
try {
|
||||
ldapIdentityStore.validatePassword(ldapUser, password);
|
||||
return true;
|
||||
} catch (AuthenticationException ae) {
|
||||
|
||||
// Check if any mapper provides callback for handle LDAP AuthenticationException
|
||||
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(getModel().getId());
|
||||
boolean processed = false;
|
||||
for (UserFederationMapperModel mapperModel : federationMappers) {
|
||||
LDAPFederationMapper ldapMapper = getMapper(mapperModel);
|
||||
processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm);
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
|
|||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
||||
import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
@ -188,6 +189,12 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
|||
UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
|
||||
UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
|
||||
realm.addUserFederationMapper(mapperModel);
|
||||
|
||||
// MSAD specific mapper for account state propagation
|
||||
if (activeDirectory) {
|
||||
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", newProviderModel.getId(), MSADUserAccountControlMapperFactory.PROVIDER_ID);
|
||||
realm.addUserFederationMapper(mapperModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -66,6 +66,10 @@ public class LDAPObject {
|
|||
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public void removeReadOnlyAttributeName(String readOnlyAttribute) {
|
||||
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public String getRdnAttributeName() {
|
||||
return rdnAttributeName;
|
||||
}
|
||||
|
|
|
@ -189,4 +189,8 @@ public class LDAPQuery {
|
|||
return this.conditions;
|
||||
}
|
||||
|
||||
public LDAPFederationProvider getLdapProvider() {
|
||||
return ldapFedProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.idm.store;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -65,8 +67,9 @@ public interface IdentityStore {
|
|||
*
|
||||
* @param user Keycloak user
|
||||
* @param password Ldap password
|
||||
* @throws AuthenticationException if authentication is not successful
|
||||
*/
|
||||
boolean validatePassword(LDAPObject user, String password);
|
||||
void validatePassword(LDAPObject user, String password) throws AuthenticationException;
|
||||
|
||||
/**
|
||||
* Updates the specified credential value.
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
|
|||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.Attribute;
|
||||
|
@ -173,18 +174,14 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
// *************** CREDENTIALS AND USER SPECIFIC STUFF
|
||||
|
||||
@Override
|
||||
public boolean validatePassword(LDAPObject user, String password) {
|
||||
public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
|
||||
String userDN = user.getDn().toString();
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Using DN [%s] for authentication of user", userDN);
|
||||
}
|
||||
|
||||
if (operationManager.authenticate(userDN, password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
operationManager.authenticate(userDN, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -225,15 +222,6 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
|
||||
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
|
||||
|
||||
// Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
|
||||
// TODO: Remove and use mapper instead
|
||||
if (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
|
||||
BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
|
||||
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));
|
||||
|
||||
logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
|
||||
}
|
||||
|
||||
operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
|
||||
} catch (Exception e) {
|
||||
throw new ModelException(e);
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Map;
|
|||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Binding;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
|
@ -320,15 +321,15 @@ public class LDAPOperationManager {
|
|||
*
|
||||
* @param dn
|
||||
* @param password
|
||||
* @throws AuthenticationException if authentication is not successful
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean authenticate(String dn, String password) {
|
||||
public void authenticate(String dn, String password) throws AuthenticationException {
|
||||
InitialContext authCtx = null;
|
||||
|
||||
try {
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new Exception("Empty password used");
|
||||
throw new AuthenticationException("Empty password used");
|
||||
}
|
||||
|
||||
Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
|
||||
|
@ -342,13 +343,15 @@ public class LDAPOperationManager {
|
|||
|
||||
authCtx = new InitialLdapContext(env, null);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
} catch (AuthenticationException ae) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(e, "Authentication failed for DN [%s]", dn);
|
||||
logger.debugf(ae, "Authentication failed for DN [%s]", dn);
|
||||
}
|
||||
|
||||
return false;
|
||||
throw ae;
|
||||
} catch (Exception e) {
|
||||
logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn);
|
||||
throw new AuthenticationException("Unexpected exception when validating password of user");
|
||||
} finally {
|
||||
if (authCtx != null) {
|
||||
try {
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.keycloak.federation.ldap.mappers;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -70,6 +72,10 @@ public abstract class AbstractLDAPFederationMapper {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
|
||||
String paramm = mapperModel.getConfig().get(paramName);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -59,4 +61,17 @@ public interface LDAPFederationMapper extends UserFederationMapper {
|
|||
* @param query
|
||||
*/
|
||||
void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
|
||||
|
||||
/**
|
||||
* Called when LDAP authentication of specified user fails. If any mapper returns true from this method, AuthenticationException won't be rethrown!
|
||||
*
|
||||
* @param mapperModel
|
||||
* @param ldapProvider
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param ldapUser
|
||||
* @param ldapException
|
||||
* @return true if mapper processed the AuthenticationException and did some actions based on that. In that case, AuthenticationException won't be rethrown!
|
||||
*/
|
||||
boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.mappers;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -55,7 +57,7 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper {
|
|||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
// Improve if needed
|
||||
getDelegate(mapperModel, null, null).beforeLDAPQuery(query);
|
||||
getDelegate(mapperModel, query.getLdapProvider(), null).beforeLDAPQuery(query);
|
||||
}
|
||||
|
||||
|
||||
|
@ -64,6 +66,11 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper {
|
|||
return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm) {
|
||||
return getDelegate(mapperModel, ldapProvider, realm).onAuthenticationFailure(ldapUser, user, ldapException);
|
||||
}
|
||||
|
||||
private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) {
|
||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
||||
return factory.createMapper(mapperModel, ldapProvider, realm);
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* Mapper specific to MSAD. It's able to read the userAccountControl and pwdLastSet attributes and set actions in Keycloak based on that.
|
||||
* It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 )
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(MSADUserAccountControlMapper.class);
|
||||
|
||||
private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*");
|
||||
|
||||
public MSADUserAccountControlMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET);
|
||||
query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
|
||||
// This needs to be read-only and can be set to writable just on demand
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
if (ldapProvider.getEditMode() != UserFederationProvider.EditMode.WRITABLE) {
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
return new MSADUserModelDelegate(delegate, ldapUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
|
||||
String exceptionMessage = ldapException.getMessage();
|
||||
Matcher m = AUTH_EXCEPTION_REGEX.matcher(exceptionMessage);
|
||||
if (m.matches()) {
|
||||
String errorCode = m.group(1);
|
||||
return processAuthErrorCode(errorCode, user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean processAuthErrorCode(String errorCode, UserModel user) {
|
||||
logger.debugf("MSAD Error code is '%s' after failed LDAP login of user", errorCode, user.getUsername());
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
|
||||
if (errorCode.equals("532") || errorCode.equals("773")) {
|
||||
// User needs to change his MSAD password. Allow him to login, but add UPDATE_PASSWORD required action
|
||||
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
return true;
|
||||
} else if (errorCode.equals("533")) {
|
||||
// User is disabled in MSAD. Set him to disabled in KC as well
|
||||
user.setEnabled(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public class MSADUserModelDelegate extends UserModelDelegate {
|
||||
|
||||
private final LDAPObject ldapUser;
|
||||
|
||||
public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
|
||||
super(delegate);
|
||||
this.ldapUser = ldapUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
boolean kcEnabled = super.isEnabled();
|
||||
|
||||
if (getPwdLastSet() > 0) {
|
||||
// Merge KC and MSAD
|
||||
return kcEnabled && !getUserAccountControl().has(UserAccountControl.ACCOUNTDISABLE);
|
||||
} else {
|
||||
// If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway
|
||||
return kcEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
// Always update DB
|
||||
super.setEnabled(enabled);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && getPwdLastSet() > 0) {
|
||||
logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString());
|
||||
|
||||
UserAccountControl control = getUserAccountControl();
|
||||
if (enabled) {
|
||||
control.remove(UserAccountControl.ACCOUNTDISABLE);
|
||||
} else {
|
||||
control.add(UserAccountControl.ACCOUNTDISABLE);
|
||||
}
|
||||
|
||||
updateUserAccountControl(control);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCredential(UserCredentialModel cred) {
|
||||
// Update LDAP password first
|
||||
super.updateCredential(cred);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
|
||||
|
||||
UserAccountControl control = getUserAccountControl();
|
||||
control.remove(UserAccountControl.PASSWD_NOTREQD);
|
||||
control.remove(UserAccountControl.PASSWORD_EXPIRED);
|
||||
|
||||
if (super.isEnabled()) {
|
||||
control.remove(UserAccountControl.ACCOUNTDISABLE);
|
||||
}
|
||||
|
||||
updateUserAccountControl(control);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(RequiredAction action) {
|
||||
String actionName = action.name();
|
||||
addRequiredAction(actionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(String action) {
|
||||
// Always update DB
|
||||
super.addRequiredAction(action);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
|
||||
logger.debugf("Going to propagate required action UPDATE_PASSWORD to MSAD for ldap user '%s' ", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0");
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(RequiredAction action) {
|
||||
String actionName = action.name();
|
||||
removeRequiredAction(actionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(String action) {
|
||||
// Always update DB
|
||||
super.removeRequiredAction(action);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
|
||||
|
||||
// Don't set pwdLastSet in MSAD when it is new user
|
||||
UserAccountControl accountControl = getUserAccountControl();
|
||||
if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) {
|
||||
logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRequiredActions() {
|
||||
Set<String> requiredActions = super.getRequiredActions();
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
|
||||
if (getPwdLastSet() == 0 || getUserAccountControl().has(UserAccountControl.PASSWORD_EXPIRED)) {
|
||||
requiredActions = new HashSet<>(requiredActions);
|
||||
requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString());
|
||||
return requiredActions;
|
||||
}
|
||||
}
|
||||
|
||||
return requiredActions;
|
||||
}
|
||||
|
||||
protected long getPwdLastSet() {
|
||||
String pwdLastSet = ldapUser.getAttributeAsString(LDAPConstants.PWD_LAST_SET);
|
||||
return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet);
|
||||
}
|
||||
|
||||
protected UserAccountControl getUserAccountControl() {
|
||||
String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl);
|
||||
return new UserAccountControl(longValue);
|
||||
}
|
||||
|
||||
// Update user in LDAP
|
||||
protected void updateUserAccountControl(UserAccountControl accountControl) {
|
||||
String userAccountControlValue = String.valueOf(accountControl.getValue());
|
||||
logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue);
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MSADUserAccountControlMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Mapper specific to MSAD. It's able to integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using userAccountControl and pwdLastSet MSAD attributes for that. " +
|
||||
"For example if pwdLastSet is 0, the Keycloak user is required to update password, if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc. Mapper is also able to handle exception code from LDAP user authentication.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return ATTRIBUTE_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "MSAD User Account Control";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new MSADUserAccountControlMapper(mapperModel, federationProvider, realm);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
/**
|
||||
* See https://support.microsoft.com/en-us/kb/305144
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserAccountControl {
|
||||
|
||||
public static final long SCRIPT = 0x0001l;
|
||||
public static final long ACCOUNTDISABLE = 0x0002l;
|
||||
public static final long HOMEDIR_REQUIRED = 0x0008l;
|
||||
public static final long LOCKOUT = 0x0010l;
|
||||
public static final long PASSWD_NOTREQD = 0x0020l;
|
||||
public static final long PASSWD_CANT_CHANGE = 0x0040l;
|
||||
public static final long ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080l;
|
||||
public static final long TEMP_DUPLICATE_ACCOUNT = 0x0100l;
|
||||
public static final long NORMAL_ACCOUNT = 0x0200l;
|
||||
public static final long INTERDOMAIN_TRUST_ACCOUNT = 0x0800l;
|
||||
public static final long WORKSTATION_TRUST_ACCOUNT = 0x1000l;
|
||||
public static final long SERVER_TRUST_ACCOUNT = 0x2000l;
|
||||
public static final long DONT_EXPIRE_PASSWORD = 0x10000l;
|
||||
public static final long MNS_LOGON_ACCOUNT = 0x20000l;
|
||||
public static final long SMARTCARD_REQUIRED = 0x40000l;
|
||||
public static final long TRUSTED_FOR_DELEGATION = 0x80000l;
|
||||
public static final long NOT_DELEGATED = 0x100000l;
|
||||
public static final long USE_DES_KEY_ONLY = 0x200000l;
|
||||
public static final long DONT_REQ_PREAUTH = 0x400000l;
|
||||
public static final long PASSWORD_EXPIRED = 0x800000l;
|
||||
public static final long TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000l;
|
||||
public static final long PARTIAL_SECRETS_ACCOUNT = 0x04000000l;
|
||||
|
||||
private long value;
|
||||
|
||||
public UserAccountControl(long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean has(long feature) {
|
||||
return (this.value & feature) > 0;
|
||||
}
|
||||
|
||||
public void add(long feature) {
|
||||
if (!has(feature)) {
|
||||
this.value += feature;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(long feature) {
|
||||
if (has(feature)) {
|
||||
this.value -= feature;
|
||||
}
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -3,3 +3,4 @@ org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
|
|||
org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory
|
|
@ -183,14 +183,6 @@
|
|||
</div>
|
||||
<kc-tooltip>Does the LDAP server support pagination.</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix" data-ng-show="instance.config.vendor === 'ad' ">
|
||||
<label class="col-md-2 control-label" for="userAccountControlsAfterPasswordUpdate">Enable Account After Password Update</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="instance.config.userAccountControlsAfterPasswordUpdate" name="userAccountControlsAfterPasswordUpdate" id="userAccountControlsAfterPasswordUpdate" onoffswitch />
|
||||
</div>
|
||||
<kc-tooltip>Useful just for Active Directory. If enabled, then Keycloak will always set
|
||||
Active Directory userAccountControl attribute to 512 after password update. This would mean that particular user will be enabled in Active Directory</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package org.keycloak.migration;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -11,7 +9,7 @@ public interface MigrationModel {
|
|||
/**
|
||||
* Must have the form of major.minor.micro as the version is parsed and numbers are compared
|
||||
*/
|
||||
public static final String LATEST_VERSION = "1.7.0";
|
||||
String LATEST_VERSION = "1.8.0";
|
||||
|
||||
String getStoredVersion();
|
||||
void setStoredVersion(String version);
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.keycloak.migration.migrators.MigrateTo1_4_0;
|
|||
import org.keycloak.migration.migrators.MigrateTo1_5_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_6_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_7_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_8_0;
|
||||
import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
|
@ -61,6 +62,12 @@ public class MigrationModelManager {
|
|||
}
|
||||
new MigrateTo1_7_0().migrate(session);
|
||||
}
|
||||
if (stored == null || stored.lessThan(MigrateTo1_8_0.VERSION)) {
|
||||
if (stored != null) {
|
||||
logger.debug("Migrating older model to 1.8.0 updates");
|
||||
}
|
||||
new MigrateTo1_8_0().migrate(session);
|
||||
}
|
||||
|
||||
model.setStoredVersion(MigrationModel.LATEST_VERSION);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package org.keycloak.migration.migrators;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.keycloak.migration.ModelVersion;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MigrateTo1_8_0 {
|
||||
|
||||
public static final ModelVersion VERSION = new ModelVersion("1.8.0");
|
||||
|
||||
public void migrate(KeycloakSession session) {
|
||||
List<RealmModel> realms = session.realms().getRealms();
|
||||
for (RealmModel realm : realms) {
|
||||
|
||||
List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders();
|
||||
for (UserFederationProviderModel fedProvider : federationProviders) {
|
||||
|
||||
if (fedProvider.getProviderName().equals(LDAPConstants.LDAP_PROVIDER)) {
|
||||
Map<String, String> config = fedProvider.getConfig();
|
||||
|
||||
if (isActiveDirectory(config)) {
|
||||
|
||||
// Create mapper for MSAD account controls
|
||||
if (realm.getUserFederationMapperByName(fedProvider.getId(), "MSAD account controls") == null) {
|
||||
UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", fedProvider.getId(), LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER);
|
||||
realm.addUserFederationMapper(mapperModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isActiveDirectory(Map<String, String> ldapConfig) {
|
||||
String vendor = ldapConfig.get(LDAPConstants.VENDOR);
|
||||
return vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ package org.keycloak.models;
|
|||
public class LDAPConstants {
|
||||
|
||||
public static final String LDAP_PROVIDER = "ldap";
|
||||
public static final String MSAD_USER_ACCOUNT_CONTROL_MAPPER = "msad-user-account-control-mapper";
|
||||
|
||||
public static final String VENDOR = "vendor";
|
||||
public static final String VENDOR_RHDS = "rhds";
|
||||
|
@ -43,9 +44,6 @@ public class LDAPConstants {
|
|||
// Config option to specify if registrations will be synced or not
|
||||
public static final String SYNC_REGISTRATIONS = "syncRegistrations";
|
||||
|
||||
// Applicable just for active directory
|
||||
public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate";
|
||||
|
||||
// Custom user search filter
|
||||
public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter";
|
||||
|
||||
|
@ -53,9 +51,6 @@ public class LDAPConstants {
|
|||
public static final String LDAP_ID = "LDAP_ID";
|
||||
public static final String LDAP_ENTRY_DN = "LDAP_ENTRY_DN";
|
||||
|
||||
// String used in config to divide more possible values (for example more userDns), which are saved in DB as single string
|
||||
public static final String CONFIG_DIVIDER = ":::";
|
||||
|
||||
// Those are forked from Picketlink
|
||||
public static final String GIVENNAME = "givenName";
|
||||
public static final String CN = "cn";
|
||||
|
@ -73,6 +68,8 @@ public class LDAPConstants {
|
|||
public static final String GROUP_OF_NAMES = "groupOfNames";
|
||||
public static final String GROUP_OF_ENTRIES = "groupOfEntries";
|
||||
public static final String GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames";
|
||||
public static final String USER_ACCOUNT_CONTROL = "userAccountControl";
|
||||
public static final String PWD_LAST_SET = "pwdLastSet";
|
||||
|
||||
public static final String COMMA = ",";
|
||||
public static final String EQUAL = "=";
|
||||
|
|
|
@ -39,7 +39,6 @@ public class LDAPTestConfiguration {
|
|||
PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute");
|
||||
PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute");
|
||||
PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes");
|
||||
PROP_MAPPINGS.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "idm.test.ldap.user.account.controls.after.password.update");
|
||||
PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
|
||||
|
||||
PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication");
|
||||
|
@ -62,7 +61,6 @@ public class LDAPTestConfiguration {
|
|||
DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
|
||||
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
|
||||
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null);
|
||||
DEFAULT_VALUES.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "false");
|
||||
DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString());
|
||||
|
||||
DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");
|
||||
|
|
|
@ -40,7 +40,6 @@ public class LDAPTestConfiguration {
|
|||
PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute");
|
||||
PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute");
|
||||
PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes");
|
||||
PROP_MAPPINGS.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "idm.test.ldap.user.account.controls.after.password.update");
|
||||
PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
|
||||
|
||||
PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication");
|
||||
|
@ -63,7 +62,6 @@ public class LDAPTestConfiguration {
|
|||
DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
|
||||
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
|
||||
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null);
|
||||
DEFAULT_VALUES.put(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE, "false");
|
||||
DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString());
|
||||
|
||||
DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");
|
||||
|
|
|
@ -749,7 +749,7 @@ public class FederationProvidersIntegrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testUnsynced() {
|
||||
public void testUnsynced() throws Exception {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||
|
|
Loading…
Reference in a new issue