Merge pull request #1976 from mposolda/master

KEYCLOAK-2178 KEYCLOAK-1744 Added MSADUserAccountControlMapper. Remov…
This commit is contained in:
Marek Posolda 2016-01-05 13:16:32 +01:00
commit d3247bc66a
23 changed files with 516 additions and 54 deletions

View file

@ -113,12 +113,6 @@ public class LDAPConfig {
return uuidAttrName; 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() { public boolean isPagination() {
String pagination = config.get(LDAPConstants.PAGINATION); String pagination = config.get(LDAPConstants.PAGINATION);
return pagination==null ? false : Boolean.parseBoolean(pagination); return pagination==null ? false : Boolean.parseBoolean(pagination);

View file

@ -41,6 +41,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.naming.AuthenticationException;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -366,7 +368,22 @@ public class LDAPFederationProvider implements UserFederationProvider {
} else { } else {
// Use Naming LDAP API // Use Naming LDAP API
LDAPObject ldapUser = loadAndValidateUser(realm, user); 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;
}
} }
} }

View file

@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -188,6 +189,12 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP, UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addUserFederationMapper(mapperModel); 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);
}
} }

View file

@ -66,6 +66,10 @@ public class LDAPObject {
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase()); readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
} }
public void removeReadOnlyAttributeName(String readOnlyAttribute) {
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
}
public String getRdnAttributeName() { public String getRdnAttributeName() {
return rdnAttributeName; return rdnAttributeName;
} }

View file

@ -189,4 +189,8 @@ public class LDAPQuery {
return this.conditions; return this.conditions;
} }
public LDAPFederationProvider getLdapProvider() {
return ldapFedProvider;
}
} }

View file

@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.idm.store;
import java.util.List; import java.util.List;
import javax.naming.AuthenticationException;
import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.LDAPConfig;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@ -65,8 +67,9 @@ public interface IdentityStore {
* *
* @param user Keycloak user * @param user Keycloak user
* @param password Ldap password * @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. * Updates the specified credential value.

View file

@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration; import javax.naming.NamingEnumeration;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.directory.Attribute; import javax.naming.directory.Attribute;
@ -173,18 +174,14 @@ public class LDAPIdentityStore implements IdentityStore {
// *************** CREDENTIALS AND USER SPECIFIC STUFF // *************** CREDENTIALS AND USER SPECIFIC STUFF
@Override @Override
public boolean validatePassword(LDAPObject user, String password) { public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
String userDN = user.getDn().toString(); String userDN = user.getDn().toString();
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.tracef("Using DN [%s] for authentication of user", userDN); logger.tracef("Using DN [%s] for authentication of user", userDN);
} }
if (operationManager.authenticate(userDN, password)) { operationManager.authenticate(userDN, password);
return true;
}
return false;
} }
@Override @Override
@ -225,15 +222,6 @@ 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));
// 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[] {})); operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
} catch (Exception e) { } catch (Exception e) {
throw new ModelException(e); throw new ModelException(e);

View file

@ -12,6 +12,7 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import javax.naming.AuthenticationException;
import javax.naming.Binding; import javax.naming.Binding;
import javax.naming.Context; import javax.naming.Context;
import javax.naming.InitialContext; import javax.naming.InitialContext;
@ -320,15 +321,15 @@ public class LDAPOperationManager {
* *
* @param dn * @param dn
* @param password * @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; InitialContext authCtx = null;
try { try {
if (password == null || password.isEmpty()) { 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); Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
@ -342,13 +343,15 @@ public class LDAPOperationManager {
authCtx = new InitialLdapContext(env, null); authCtx = new InitialLdapContext(env, null);
return true; } catch (AuthenticationException ae) {
} catch (Exception e) {
if (logger.isDebugEnabled()) { 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 { } finally {
if (authCtx != null) { if (authCtx != null) {
try { try {

View file

@ -3,6 +3,8 @@ package org.keycloak.federation.ldap.mappers;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.naming.AuthenticationException;
import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@ -70,6 +72,10 @@ public abstract class AbstractLDAPFederationMapper {
return Collections.emptyList(); return Collections.emptyList();
} }
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
return false;
}
public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
String paramm = mapperModel.getConfig().get(paramName); String paramm = mapperModel.getConfig().get(paramName);

View file

@ -1,5 +1,7 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import javax.naming.AuthenticationException;
import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@ -59,4 +61,17 @@ public interface LDAPFederationMapper extends UserFederationMapper {
* @param query * @param query
*/ */
void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery 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);
} }

View file

@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.mappers;
import java.util.List; import java.util.List;
import javax.naming.AuthenticationException;
import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
@ -55,7 +57,7 @@ public class LDAPFederationMapperBridge implements LDAPFederationMapper {
@Override @Override
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
// Improve if needed // 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); 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) { private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) {
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider; LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
return factory.createMapper(mapperModel, ldapProvider, realm); return factory.createMapper(mapperModel, ldapProvider, realm);

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -2,4 +2,5 @@ org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory
org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory

View file

@ -183,14 +183,6 @@
</div> </div>
<kc-tooltip>Does the LDAP server support pagination.</kc-tooltip> <kc-tooltip>Does the LDAP server support pagination.</kc-tooltip>
</div> </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>
<fieldset> <fieldset>

View file

@ -1,7 +1,5 @@
package org.keycloak.migration; package org.keycloak.migration;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @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 * 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(); String getStoredVersion();
void setStoredVersion(String version); void setStoredVersion(String version);

View file

@ -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_5_0;
import org.keycloak.migration.migrators.MigrateTo1_6_0; import org.keycloak.migration.migrators.MigrateTo1_6_0;
import org.keycloak.migration.migrators.MigrateTo1_7_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.migration.migrators.MigrationTo1_2_0_CR1;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -61,6 +62,12 @@ public class MigrationModelManager {
} }
new MigrateTo1_7_0().migrate(session); 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); model.setStoredVersion(MigrationModel.LATEST_VERSION);
} }

View file

@ -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);
}
}

View file

@ -6,6 +6,7 @@ package org.keycloak.models;
public class LDAPConstants { public class LDAPConstants {
public static final String LDAP_PROVIDER = "ldap"; 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 = "vendor";
public static final String VENDOR_RHDS = "rhds"; 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 // Config option to specify if registrations will be synced or not
public static final String SYNC_REGISTRATIONS = "syncRegistrations"; 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 // Custom user search filter
public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter"; 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_ID = "LDAP_ID";
public static final String LDAP_ENTRY_DN = "LDAP_ENTRY_DN"; 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 // Those are forked from Picketlink
public static final String GIVENNAME = "givenName"; public static final String GIVENNAME = "givenName";
public static final String CN = "cn"; 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_NAMES = "groupOfNames";
public static final String GROUP_OF_ENTRIES = "groupOfEntries"; public static final String GROUP_OF_ENTRIES = "groupOfEntries";
public static final String GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames"; 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 COMMA = ",";
public static final String EQUAL = "="; public static final String EQUAL = "=";

View file

@ -39,7 +39,6 @@ public class LDAPTestConfiguration {
PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute"); 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.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_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(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication"); 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.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null); DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, 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(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString());
DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");

View file

@ -40,7 +40,6 @@ public class LDAPTestConfiguration {
PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute"); 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.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_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(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication"); 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.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null); DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, 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(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString());
DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");

View file

@ -749,7 +749,7 @@ public class FederationProvidersIntegrationTest {
} }
@Test @Test
public void testUnsynced() { public void testUnsynced() throws Exception {
KeycloakSession session = keycloakRule.startSession(); KeycloakSession session = keycloakRule.startSession();
try { try {
RealmModel appRealm = session.realms().getRealmByName("test"); RealmModel appRealm = session.realms().getRealmByName("test");