KEYCLOAK-16468 Support for deny list of metadata attributes not updateable by account REST and admin REST
(cherry picked from commit 79db549c9d561b8d5efe3596370190c4da47e4e1) (cherry picked from commit bf4401cddd5d3b0033820b1cb4904bd1c8b56db9)
This commit is contained in:
parent
eac3329d22
commit
dae4a3eaf2
21 changed files with 762 additions and 49 deletions
|
@ -54,6 +54,8 @@ public class Messages {
|
|||
|
||||
public static final String MISSING_USERNAME = "missingUsernameMessage";
|
||||
|
||||
public static final String UPDATE_READ_ONLY_ATTRIBUTES_REJECTED = "updateReadOnlyAttributesRejectedMessage";
|
||||
|
||||
public static final String MISSING_PASSWORD = "missingPasswordMessage";
|
||||
|
||||
public static final String MISSING_TOTP = "missingTotpMessage";
|
||||
|
|
|
@ -170,6 +170,11 @@ public class AccountRestService {
|
|||
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
|
||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS))
|
||||
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
// Here should be possibility to somehow return all errors?
|
||||
String firstErrorMessage = result.getErrors().get(0).getFailedValidations().get(0).getErrorType();
|
||||
return ErrorResponse.error(firstErrorMessage, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
UserUpdateHelper.updateAccount(realm, user, updatedUser);
|
||||
|
|
|
@ -71,8 +71,16 @@ import org.keycloak.services.resources.account.AccountFormService;
|
|||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
|
||||
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
|
||||
import org.keycloak.userprofile.validation.AttributeValidationResult;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.validation.ValidationResult;
|
||||
import org.keycloak.utils.ProfileHelper;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
@ -166,6 +174,10 @@ public class UserResource {
|
|||
}
|
||||
}
|
||||
|
||||
Response response = validateUserProfile(user, rep, session);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
updateUserFromRep(user, rep, session, true);
|
||||
RepresentationToModel.createCredentials(rep, session, realm, user, true);
|
||||
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success();
|
||||
|
@ -189,9 +201,27 @@ public class UserResource {
|
|||
}
|
||||
}
|
||||
|
||||
public static void updateUserFromRep(UserModel user, UserRepresentation rep, KeycloakSession session, boolean removeMissingRequiredActions) {
|
||||
public static Response validateUserProfile(UserModel user, UserRepresentation rep, KeycloakSession session) {
|
||||
UserProfile updatedUser = new UserRepresentationUserProfile(rep);
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
|
||||
UserProfileValidationResult result = profileProvider.validate(DefaultUserProfileContext.forUserResource(user), updatedUser);
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
for (AttributeValidationResult attrValidation : result.getErrors()) {
|
||||
StringBuilder s = new StringBuilder("Failed to update attribute " + attrValidation.getField() + ": ");
|
||||
for (ValidationResult valResult : attrValidation.getFailedValidations()) {
|
||||
s.append(valResult.getErrorType() + ", ");
|
||||
}
|
||||
logger.warn(s);
|
||||
}
|
||||
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
UserUpdateHelper.updateUserResource(session.getContext().getRealm(), user, new UserRepresentationUserProfile(rep));
|
||||
public static void updateUserFromRep(UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
|
||||
boolean removeMissingRequiredActions = isUpdateExistingUser;
|
||||
UserUpdateHelper.updateUserResource(session.getContext().getRealm(), user, new UserRepresentationUserProfile(rep), isUpdateExistingUser);
|
||||
|
||||
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
|
||||
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
|
||||
|
|
|
@ -147,6 +147,11 @@ public class UsersResource {
|
|||
}
|
||||
|
||||
try {
|
||||
Response response = UserResource.validateUserProfile(null, rep, session);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
|
||||
UserModel user = session.users().addUser(realm, username);
|
||||
|
||||
UserResource.updateUserFromRep(user, rep, session, false);
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -32,10 +37,14 @@ import org.keycloak.userprofile.validation.ValidationChainBuilder;
|
|||
public class LegacyUserProfileProvider implements UserProfileProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LegacyUserProfileProvider.class);
|
||||
private KeycloakSession session;
|
||||
private final KeycloakSession session;
|
||||
private final Pattern readOnlyAttributes;
|
||||
private final Pattern adminReadOnlyAttributes;
|
||||
|
||||
public LegacyUserProfileProvider(KeycloakSession session) {
|
||||
public LegacyUserProfileProvider(KeycloakSession session, Pattern readOnlyAttributes, Pattern adminReadOnlyAttributes) {
|
||||
this.session = session;
|
||||
this.readOnlyAttributes = readOnlyAttributes;
|
||||
this.adminReadOnlyAttributes = adminReadOnlyAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -50,18 +59,22 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
|
|||
ValidationChainBuilder builder = ValidationChainBuilder.builder();
|
||||
switch (updateContext.getUpdateEvent()) {
|
||||
case UserResource:
|
||||
addReadOnlyAttributeValidators(builder, adminReadOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
case IdpReview:
|
||||
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername());
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
case Account:
|
||||
case RegistrationProfile:
|
||||
case UpdateProfile:
|
||||
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername() && realm.isEditUsernameAllowed());
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
addSessionValidators(builder);
|
||||
break;
|
||||
case RegistrationUserCreation:
|
||||
addUserCreationValidators(builder);
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
}
|
||||
return new UserProfileValidationResult(builder.build().validate(updateContext,updatedProfile));
|
||||
|
@ -72,16 +85,16 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
|
|||
|
||||
if (realm.isRegistrationEmailAsUsername()) {
|
||||
builder.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.addValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.doesEmailExist(session)).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.doesEmailExist(session)).build()
|
||||
.build();
|
||||
|
||||
|
||||
} else {
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addValidationFunction(Messages.MISSING_USERNAME, StaticValidators.isBlank())
|
||||
.addValidationFunction(Messages.USERNAME_EXISTS,
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_USERNAME, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS,
|
||||
(value, o) -> session.users().getUserByUsername(value, realm) == null)
|
||||
.build();
|
||||
}
|
||||
|
@ -90,30 +103,48 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
|
|||
private void addBasicValidators(ValidationChainBuilder builder, boolean userNameExistsCondition) {
|
||||
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addValidationFunction(Messages.MISSING_USERNAME, StaticValidators.checkUsernameExists(userNameExistsCondition)).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_USERNAME, StaticValidators.checkUsernameExists(userNameExistsCondition)).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.FIRST_NAME)
|
||||
.addValidationFunction(Messages.MISSING_FIRST_NAME, StaticValidators.isBlank()).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_FIRST_NAME, StaticValidators.isBlank()).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.LAST_NAME)
|
||||
.addValidationFunction(Messages.MISSING_LAST_NAME, StaticValidators.isBlank()).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_LAST_NAME, StaticValidators.isBlank()).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addSessionValidators(ValidationChainBuilder builder) {
|
||||
RealmModel realm = this.session.getContext().getRealm();
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.userNameExists(session))
|
||||
.addValidationFunction(Messages.READ_ONLY_USERNAME, StaticValidators.isUserMutable(realm)).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.userNameExists(session))
|
||||
.addSingleAttributeValueValidationFunction(Messages.READ_ONLY_USERNAME, StaticValidators.isUserMutable(realm)).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.isEmailDuplicated(session))
|
||||
.addValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.doesEmailExistAsUsername(session)).build()
|
||||
.addSingleAttributeValueValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.isEmailDuplicated(session))
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.doesEmailExistAsUsername(session)).build()
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addReadOnlyAttributeValidators(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrs, UserProfileContext updateContext, UserProfile updatedProfile) {
|
||||
addValidatorsForAllAttributeOfUser(builder, configuredReadOnlyAttrs, updatedProfile);
|
||||
addValidatorsForAllAttributeOfUser(builder, configuredReadOnlyAttrs, updateContext.getCurrentProfile());
|
||||
}
|
||||
|
||||
|
||||
private void addValidatorsForAllAttributeOfUser(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrsPattern, UserProfile profile) {
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
profile.getAttributes().keySet().stream()
|
||||
.filter(currentAttrName -> configuredReadOnlyAttrsPattern.matcher(currentAttrName).find())
|
||||
.forEach((currentAttrName) ->
|
||||
builder.addAttributeValidator().forAttribute(currentAttrName)
|
||||
.addValidationFunction(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, StaticValidators.isAttributeUnchanged(currentAttrName)).build()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,14 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
@ -26,18 +34,50 @@ import org.keycloak.models.KeycloakSessionFactory;
|
|||
*/
|
||||
public class LegacyUserProfileProviderFactory implements UserProfileProviderFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LegacyUserProfileProviderFactory.class);
|
||||
|
||||
UserProfileProvider provider;
|
||||
|
||||
// Attributes, which can't be updated by user himself
|
||||
private Pattern readOnlyAttributesPattern;
|
||||
|
||||
// Attributes, which can't be updated by administrator
|
||||
private Pattern adminReadOnlyAttributesPattern;
|
||||
|
||||
private String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED" };
|
||||
private String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
|
||||
|
||||
@Override
|
||||
public UserProfileProvider create(KeycloakSession session) {
|
||||
provider = new LegacyUserProfileProvider(session);
|
||||
provider = new LegacyUserProfileProvider(session, readOnlyAttributesPattern, adminReadOnlyAttributesPattern);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
this.readOnlyAttributesPattern = getRegexPatternString(config, "read-only-attributes", DEFAULT_READ_ONLY_ATTRIBUTES);
|
||||
this.adminReadOnlyAttributesPattern = getRegexPatternString(config, "admin-read-only-attributes", DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES);
|
||||
}
|
||||
|
||||
private Pattern getRegexPatternString(Config.Scope config, String configKey, String[] builtinReadOnlyAttributes) {
|
||||
String[] readOnlyAttributesCfg = config.getArray(configKey);
|
||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||
if (readOnlyAttributesCfg != null) {
|
||||
List<String> configured = Arrays.asList(readOnlyAttributesCfg);
|
||||
logger.infof("Configured %s: %s", configKey, configured);
|
||||
readOnlyAttributes.addAll(configured);
|
||||
}
|
||||
|
||||
String regexStr = readOnlyAttributes.stream()
|
||||
.map(configAttrName -> configAttrName.endsWith("*")
|
||||
? "^" + Pattern.quote(configAttrName.substring(0, configAttrName.length() - 1)) + ".*$"
|
||||
: "^" + Pattern.quote(configAttrName ) + "$")
|
||||
.collect(Collectors.joining("|"));
|
||||
regexStr = "(?i:" + regexStr + ")";
|
||||
|
||||
logger.debugf("Regex used for %s: %s", configKey, regexStr);
|
||||
return Pattern.compile(regexStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -60,8 +60,13 @@ public class DefaultUserProfileContext implements UserProfileContext {
|
|||
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, null);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forUserResource(UserRepresentation rep) {
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.UserResource, new UserRepresentationUserProfile(rep));
|
||||
/**
|
||||
* @param currentUser if this is null, then we're creating new user. If it is not null, we're updating existing user
|
||||
* @return user profile context for the validation of user when called from admin REST API
|
||||
*/
|
||||
public static DefaultUserProfileContext forUserResource(UserModel currentUser) {
|
||||
UserProfile currentUserProfile = currentUser == null ? null : new UserModelUserProfile(currentUser);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.UserResource, currentUserProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -57,8 +57,8 @@ public class UserUpdateHelper {
|
|||
update(UserUpdateEvent.Account, realm, user, updatedProfile);
|
||||
}
|
||||
|
||||
public static void updateUserResource(RealmModel realm, UserModel user, UserProfile userRepresentationUserProfile) {
|
||||
update(UserUpdateEvent.UserResource, realm, user, userRepresentationUserProfile);
|
||||
public static void updateUserResource(RealmModel realm, UserModel user, UserProfile userRepresentationUserProfile, boolean removeExistingAttributes) {
|
||||
update(UserUpdateEvent.UserResource, realm, user, userRepresentationUserProfile.getAttributes(), removeExistingAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,7 +35,23 @@ public class AttributeValidatorBuilder {
|
|||
this.validationChainBuilder = validationChainBuilder;
|
||||
}
|
||||
|
||||
public AttributeValidatorBuilder addValidationFunction(String messageKey, BiFunction<String, UserProfileContext, Boolean> validationFunction) {
|
||||
/**
|
||||
* This method is for validating first value of the specified attribute. It is sufficient for all the single-valued attributes
|
||||
*
|
||||
* @param messageKey Key of the error message to be displayed when validation fails
|
||||
* @param validationFunction Function, which does the actual validation logic. The "String" argument is the new value of the particular attribute.
|
||||
* @return this
|
||||
*/
|
||||
public AttributeValidatorBuilder addSingleAttributeValueValidationFunction(String messageKey, BiFunction<String, UserProfileContext, Boolean> validationFunction) {
|
||||
BiFunction<List<String>, UserProfileContext, Boolean> wrappedValidationFunction = (attrValues, context) -> {
|
||||
String singleValue = attrValues == null ? null : attrValues.get(0);
|
||||
return validationFunction.apply(singleValue, context);
|
||||
};
|
||||
this.validations.add(new Validator(messageKey, wrappedValidationFunction));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeValidatorBuilder addValidationFunction(String messageKey, BiFunction<List<String>, UserProfileContext, Boolean> validationFunction) {
|
||||
this.validations.add(new Validator(messageKey, validationFunction));
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -17,21 +17,32 @@
|
|||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.LegacyUserProfileProvider;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Functions are supposed to return:
|
||||
* - true if validation success
|
||||
* - false if validation fails
|
||||
*
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class StaticValidators {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(StaticValidators.class);
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isBlank() {
|
||||
return (value, context) ->
|
||||
!Validation.isBlank(value);
|
||||
value==null || !Validation.isBlank(value);
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isEmailValid() {
|
||||
|
@ -40,18 +51,22 @@ public class StaticValidators {
|
|||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> userNameExists(KeycloakSession session) {
|
||||
return (value, context) ->
|
||||
!(context.getCurrentProfile() != null
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
return !(context.getCurrentProfile() != null
|
||||
&& !value.equals(context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME))
|
||||
&& session.users().getUserByUsername(value, session.getContext().getRealm()) != null);
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isUserMutable(RealmModel realm) {
|
||||
return (value, context) ->
|
||||
!(!realm.isEditUsernameAllowed()
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
return !(!realm.isEditUsernameAllowed()
|
||||
&& context.getCurrentProfile() != null
|
||||
&& !value.equals(context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> checkUsernameExists(boolean externalCondition) {
|
||||
|
@ -62,6 +77,7 @@ public class StaticValidators {
|
|||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> doesEmailExistAsUsername(KeycloakSession session) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(value, realm);
|
||||
|
@ -73,6 +89,7 @@ public class StaticValidators {
|
|||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isEmailDuplicated(KeycloakSession session) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(value, realm);
|
||||
|
@ -90,4 +107,15 @@ public class StaticValidators {
|
|||
&& session.users().getUserByEmail(value, session.getContext().getRealm()) != null);
|
||||
}
|
||||
|
||||
public static BiFunction<List<String>, UserProfileContext, Boolean> isAttributeUnchanged(String attributeName) {
|
||||
return (newAttrValues, context) -> {
|
||||
List<String> existingAttrValues = context.getCurrentProfile() == null ? null : context.getCurrentProfile().getAttributes().getAttribute(attributeName);
|
||||
boolean result = ObjectUtil.isEqualOrBothNull(newAttrValues, existingAttrValues);
|
||||
if (!result) {
|
||||
logger.warnf("Attempt to edit denied attribute '%s' of user '%s'", attributeName, context.getCurrentProfile() == null ? "new user" : context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileAttributes;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
|
@ -40,16 +40,14 @@ public class ValidationChain {
|
|||
List<ValidationResult> validationResults = new ArrayList<>();
|
||||
|
||||
String attributeKey = attribute.attributeKey;
|
||||
String attributeValue = updatedProfile.getAttributes().getFirstAttribute(attributeKey);
|
||||
boolean attributeChanged = false;
|
||||
List<String> attributeValues = updatedProfile.getAttributes().getAttribute(attributeKey);
|
||||
|
||||
if (attributeValue != null) {
|
||||
attributeChanged = updateContext.getCurrentProfile() != null
|
||||
&& !attributeValue.equals(updateContext.getCurrentProfile().getAttributes().getFirstAttribute(attributeKey));
|
||||
List<String> existingAttrValues = updateContext.getCurrentProfile() == null ? null : updateContext.getCurrentProfile().getAttributes().getAttribute(attributeKey);
|
||||
boolean attributeChanged = !Objects.equals(attributeValues, existingAttrValues);
|
||||
for (Validator validator : attribute.validators) {
|
||||
validationResults.add(new ValidationResult(validator.function.apply(attributeValue, updateContext), validator.errorType));
|
||||
}
|
||||
validationResults.add(new ValidationResult(validator.function.apply(attributeValues, updateContext), validator.errorType));
|
||||
}
|
||||
|
||||
overallResults.add(new AttributeValidationResult(attributeKey, attributeChanged, validationResults));
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.userprofile.validation;
|
|||
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
|
@ -26,9 +27,9 @@ import java.util.function.BiFunction;
|
|||
*/
|
||||
public class Validator {
|
||||
String errorType;
|
||||
BiFunction<String, UserProfileContext, Boolean> function;
|
||||
BiFunction<List<String>, UserProfileContext, Boolean> function;
|
||||
|
||||
public Validator(String errorType, BiFunction<String, UserProfileContext, Boolean> function) {
|
||||
public Validator(String errorType, BiFunction<List<String>, UserProfileContext, Boolean> function) {
|
||||
this.function = function;
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
|
|
@ -24,9 +24,9 @@ public class ValidationChainTest {
|
|||
public void setUp() throws Exception {
|
||||
builder = ValidationChainBuilder.builder()
|
||||
.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addValidationFunction("FAKE_FIELD_ERRORKEY", (value, updateUserProfileContext) -> !value.equals("content")).build()
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY", (value, updateUserProfileContext) -> !value.equals("content")).build()
|
||||
.addAttributeValidator().forAttribute("firstName")
|
||||
.addValidationFunction("FIRST_NAME_FIELD_ERRORKEY", (value, updateUserProfileContext) -> true).build();
|
||||
.addSingleAttributeValueValidationFunction("FIRST_NAME_FIELD_ERRORKEY", (value, updateUserProfileContext) -> true).build();
|
||||
|
||||
//default user content
|
||||
rep.singleAttribute(UserModel.FIRST_NAME, "firstName");
|
||||
|
@ -53,15 +53,15 @@ public class ValidationChainTest {
|
|||
@Test
|
||||
public void mergedConfig() {
|
||||
testchain = builder.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addValidationFunction("FAKE_FIELD_ERRORKEY_1", (value, updateUserProfileContext) -> false).build()
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY_1", (value, updateUserProfileContext) -> false).build()
|
||||
.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addValidationFunction("FAKE_FIELD_ERRORKEY_2", (value, updateUserProfileContext) -> false).build().build();
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY_2", (value, updateUserProfileContext) -> false).build().build();
|
||||
|
||||
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)));
|
||||
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY_1"));
|
||||
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY_2"));
|
||||
Assert.assertEquals(true, results.getValidationResults().stream().filter(o -> o.getField().equals("firstName")).collect(Collectors.toList()).get(0).isValid());
|
||||
Assert.assertEquals(false, results.hasAttributeChanged("firstName"));
|
||||
Assert.assertEquals(true, results.hasAttributeChanged("firstName"));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -16,3 +16,9 @@ echo ** Adding provider **
|
|||
|
||||
echo ** Adding max-detail-length to eventsStore spi **
|
||||
/subsystem=keycloak-server/spi=eventsStore/provider=jpa/:write-attribute(name=properties.max-detail-length,value=${keycloak.eventsStore.maxDetailLength:1000})
|
||||
|
||||
echo ** Adding spi=userProfile with legacy-user-profile configuration of read-only attributes **
|
||||
/subsystem=keycloak-server/spi=userProfile/:add
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright 2020 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.testsuite.account;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@AuthServerContainerExclude({REMOTE, QUARKUS}) // TODO: Enable this for quarkus and hopefully for remote as well...
|
||||
public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServiceTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AccountRestServiceReadOnlyAttributesTest.class);
|
||||
|
||||
@Test
|
||||
public void testUpdateProfileCannotUpdateReadOnlyAttributes() throws IOException {
|
||||
// Denied by default
|
||||
testAccountUpdateAttributeExpectFailure("usercertificate");
|
||||
testAccountUpdateAttributeExpectFailure("uSErCertificate");
|
||||
testAccountUpdateAttributeExpectFailure("KERBEROS_PRINCIPAL", true);
|
||||
|
||||
// Should be allowed
|
||||
testAccountUpdateAttributeExpectSuccess("noKerberos_Principal");
|
||||
testAccountUpdateAttributeExpectSuccess("KERBEROS_PRINCIPALno");
|
||||
|
||||
// Denied by default
|
||||
testAccountUpdateAttributeExpectFailure("enabled");
|
||||
testAccountUpdateAttributeExpectFailure("CREATED_TIMESTAMP", true);
|
||||
|
||||
// Should be allowed
|
||||
testAccountUpdateAttributeExpectSuccess("saml.something");
|
||||
|
||||
// Denied by configuration. "deniedFoot" is allowed as there is no wildcard
|
||||
testAccountUpdateAttributeExpectFailure("deniedfoo");
|
||||
testAccountUpdateAttributeExpectFailure("deniedFOo");
|
||||
testAccountUpdateAttributeExpectSuccess("deniedFoot");
|
||||
|
||||
// Denied by configuration. There is wildcard at the end
|
||||
testAccountUpdateAttributeExpectFailure("deniedbar");
|
||||
testAccountUpdateAttributeExpectFailure("deniedBAr");
|
||||
testAccountUpdateAttributeExpectFailure("deniedBArr");
|
||||
testAccountUpdateAttributeExpectFailure("deniedbarrier");
|
||||
|
||||
// Wildcard just at the end
|
||||
testAccountUpdateAttributeExpectSuccess("nodeniedbar");
|
||||
testAccountUpdateAttributeExpectSuccess("nodeniedBARrier");
|
||||
|
||||
// Wildcard at the end
|
||||
testAccountUpdateAttributeExpectFailure("saml.persistent.name.id.for.foo");
|
||||
testAccountUpdateAttributeExpectFailure("saml.persistent.name.id.for._foo_");
|
||||
testAccountUpdateAttributeExpectSuccess("saml.persistent.name.idafor.foo");
|
||||
|
||||
// Special characters inside should be quoted
|
||||
testAccountUpdateAttributeExpectFailure("deniedsome/thing");
|
||||
testAccountUpdateAttributeExpectFailure("deniedsome*thing");
|
||||
testAccountUpdateAttributeExpectSuccess("deniedsomeithing");
|
||||
|
||||
// Denied only for admin, but allowed for normal user
|
||||
testAccountUpdateAttributeExpectSuccess("deniedSomeAdmin");
|
||||
}
|
||||
|
||||
private void testAccountUpdateAttributeExpectFailure(String attrName) throws IOException {
|
||||
testAccountUpdateAttributeExpectFailure(attrName, false);
|
||||
}
|
||||
|
||||
private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException {
|
||||
// Attribute not yet supposed to be on the user
|
||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
Assert.assertThat(user.getAttributes().keySet(), not(contains(attrName)));
|
||||
|
||||
// Assert not possible to add the attribute to the user
|
||||
user.singleAttribute(attrName, "foo");
|
||||
updateError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Add the attribute to the user with admin REST (Case when we are adding new attribute)
|
||||
UserResource adminUserResource = null;
|
||||
org.keycloak.representations.idm.UserRepresentation adminUserRep = null;
|
||||
try {
|
||||
adminUserResource = ApiUtil.findUserByUsernameId(testRealm(), user.getUsername());
|
||||
adminUserRep = adminUserResource.toRepresentation();
|
||||
adminUserRep.singleAttribute(attrName, "foo");
|
||||
adminUserResource.update(adminUserRep);
|
||||
if (deniedForAdminAsWell) {
|
||||
Assert.fail("Not expected to update attribute " + attrName + " by admin REST API");
|
||||
}
|
||||
} catch (BadRequestException bre) {
|
||||
if (!deniedForAdminAsWell) {
|
||||
Assert.fail("Was expected to update attribute " + attrName + " by admin REST API");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update attribute of the user with account REST to the same value (Case when we are updating existing attribute) - should be fine as our attribute is not changed
|
||||
user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
Assert.assertEquals("foo", user.getAttributes().get(attrName).get(0));
|
||||
user.singleAttribute("someOtherAttr", "foo");
|
||||
user = updateAndGet(user);
|
||||
|
||||
// Update attribute of the user with account REST (Case when we are updating existing attribute
|
||||
user.singleAttribute(attrName, "foo-updated");
|
||||
updateError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Remove attribute from the user with account REST (Case when we are removing existing attribute)
|
||||
user.getAttributes().remove(attrName);
|
||||
updateError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Revert with admin REST
|
||||
adminUserRep.getAttributes().remove(attrName);
|
||||
adminUserRep.getAttributes().remove("someOtherAttr");
|
||||
adminUserResource.update(adminUserRep);
|
||||
}
|
||||
|
||||
private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException {
|
||||
// Attribute not yet supposed to be on the user
|
||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
Assert.assertThat(user.getAttributes().keySet(), not(contains(attrName)));
|
||||
|
||||
// Assert not possible to add the attribute to the user
|
||||
user.singleAttribute(attrName, "foo");
|
||||
user = updateAndGet(user);
|
||||
|
||||
// Update attribute of the user with account REST to the same value (Case when we are updating existing attribute) - should be fine as our attribute is not changed
|
||||
user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
Assert.assertEquals("foo", user.getAttributes().get(attrName).get(0));
|
||||
user.singleAttribute("someOtherAttr", "foo");
|
||||
user = updateAndGet(user);
|
||||
|
||||
// Update attribute of the user with account REST (Case when we are updating existing attribute
|
||||
user.singleAttribute(attrName, "foo-updated");
|
||||
user = updateAndGet(user);
|
||||
|
||||
// Remove attribute from the user with account REST (Case when we are removing existing attribute)
|
||||
user.getAttributes().remove(attrName);
|
||||
user = updateAndGet(user);
|
||||
|
||||
// Revert
|
||||
user.getAttributes().remove("foo");
|
||||
user.getAttributes().remove("someOtherAttr");
|
||||
user = updateAndGet(user);
|
||||
}
|
||||
|
||||
private UserRepresentation updateAndGet(UserRepresentation user) throws IOException {
|
||||
int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus();
|
||||
assertEquals(204, status);
|
||||
return SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
}
|
||||
|
||||
|
||||
private void updateError(UserRepresentation user, int expectedStatus, String expectedMessage) throws IOException {
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
assertEquals(expectedStatus, response.getStatus());
|
||||
assertEquals(expectedMessage, response.asJson(ErrorRepresentation.class).getErrorMessage());
|
||||
}
|
||||
|
||||
}
|
|
@ -40,6 +40,7 @@ import org.keycloak.credential.CredentialModel;
|
|||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
|
@ -85,6 +86,7 @@ import org.openqa.selenium.By;
|
|||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.ClientErrorException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -112,6 +114,9 @@ import static org.junit.Assert.assertThat;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.Assert.assertNames;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
||||
|
@ -1090,6 +1095,57 @@ public class UserTest extends AbstractAdminTest {
|
|||
assertNull(user1.getAttributes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@AuthServerContainerExclude(QUARKUS) // TODO: Enable for quarkus
|
||||
public void updateUserWithReadOnlyAttributes() {
|
||||
// Admin is able to update "usercertificate" attribute
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setUsername("user1");
|
||||
user1.singleAttribute("usercertificate", "foo1");
|
||||
String user1Id = createUser(user1);
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
|
||||
// Update of the user should be rejected due adding the "denied" attribute LDAP_ID
|
||||
try {
|
||||
user1.singleAttribute("usercertificate", "foo");
|
||||
user1.singleAttribute("saml.persistent.name.id.for.foo", "bar");
|
||||
user1.singleAttribute(LDAPConstants.LDAP_ID, "baz");
|
||||
updateUser(realm.users().get(user1Id), user1);
|
||||
Assert.fail("Not supposed to successfully update user");
|
||||
} catch (BadRequestException bre) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// The same test as before, but with the case-sensitivity used
|
||||
try {
|
||||
user1.getAttributes().remove(LDAPConstants.LDAP_ID);
|
||||
user1.singleAttribute("LDap_Id", "baz");
|
||||
updateUser(realm.users().get(user1Id), user1);
|
||||
Assert.fail("Not supposed to successfully update user");
|
||||
} catch (BadRequestException bre) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Attribute "deniedSomeAdmin" was denied for administrator
|
||||
try {
|
||||
user1.getAttributes().remove("LDap_Id");
|
||||
user1.singleAttribute("deniedSomeAdmin", "baz");
|
||||
updateUser(realm.users().get(user1Id), user1);
|
||||
Assert.fail("Not supposed to successfully update user");
|
||||
} catch (BadRequestException bre) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// usercertificate and saml attribute are allowed by admin
|
||||
user1.getAttributes().remove("deniedSomeAdmin");
|
||||
updateUser(realm.users().get(user1Id), user1);
|
||||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
assertEquals("foo", user1.getAttributes().get("usercertificate").get(0));
|
||||
assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0));
|
||||
assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportUserWithNullAttribute() {
|
||||
RealmRepresentation rep = loadJson(getClass().getResourceAsStream("/import/testrealm-user-null-attr.json"), RealmRepresentation.class);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.keycloak.testsuite.federation.ldap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
@ -33,16 +34,22 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.federation.kerberos.KerberosFederationProvider;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.AccountCredentialResource;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
import org.keycloak.testsuite.util.TokenUtil;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
|
@ -95,13 +102,71 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
|
|||
|
||||
@Test
|
||||
public void testGetProfile() throws IOException {
|
||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
UserRepresentation user = getProfile();
|
||||
assertEquals("John", user.getFirstName());
|
||||
assertEquals("Doe", user.getLastName());
|
||||
assertEquals("john@email.org", user.getEmail());
|
||||
assertFalse(user.isEmailVerified());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateProfile() throws IOException {
|
||||
UserRepresentation user = getProfile();
|
||||
|
||||
List<String> origLdapId = new ArrayList<>(user.getAttributes().get(LDAPConstants.LDAP_ID));
|
||||
List<String> origLdapEntryDn = new ArrayList<>(user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN));
|
||||
Assert.assertEquals(1, origLdapId.size());
|
||||
Assert.assertEquals(1, origLdapEntryDn.size());
|
||||
Assert.assertThat(user.getAttributes().keySet(), not(contains(KerberosFederationProvider.KERBEROS_PRINCIPAL)));
|
||||
|
||||
// Trying to add KERBEROS_PRINCIPAL should fail (Adding attribute, which was not yet present)
|
||||
user.setFirstName("JohnUpdated");
|
||||
user.setLastName("DoeUpdated");
|
||||
user.singleAttribute(KerberosFederationProvider.KERBEROS_PRINCIPAL, "foo");
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// The same test, but consider case sensitivity
|
||||
user.getAttributes().remove(KerberosFederationProvider.KERBEROS_PRINCIPAL);
|
||||
user.singleAttribute("KERberos_principal", "foo");
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Trying to update LDAP_ID should fail (Updating existing attribute, which was present)
|
||||
user.getAttributes().remove("KERberos_principal");
|
||||
user.setFirstName("JohnUpdated");
|
||||
user.setLastName("DoeUpdated");
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).remove(0);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).add("123");
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Trying to delete LDAP_ID should fail (Removing attribute, which was present here already)
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).remove(0);
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
user.getAttributes().remove(LDAPConstants.LDAP_ID);
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Trying to update LDAP_ENTRY_DN should fail
|
||||
user.getAttributes().put(LDAPConstants.LDAP_ID, origLdapId);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN).remove(0);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN).add("ou=foo,dc=bar");
|
||||
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
|
||||
|
||||
// Update firstName and lastName should be fine
|
||||
user.getAttributes().put(LDAPConstants.LDAP_ENTRY_DN, origLdapEntryDn);
|
||||
updateProfileExpectSuccess(user);
|
||||
|
||||
user = getProfile();
|
||||
assertEquals("JohnUpdated", user.getFirstName());
|
||||
assertEquals("DoeUpdated", user.getLastName());
|
||||
assertEquals(origLdapId, user.getAttributes().get(LDAPConstants.LDAP_ID));
|
||||
assertEquals(origLdapEntryDn, user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN));
|
||||
|
||||
// Revert
|
||||
user.setFirstName("John");
|
||||
user.setLastName("Doe");
|
||||
updateProfileExpectSuccess(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCredentials() throws IOException {
|
||||
List<AccountCredentialResource.CredentialContainer> credentials = getCredentials();
|
||||
|
@ -120,7 +185,7 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
|
|||
|
||||
|
||||
@Test
|
||||
public void testUpdateProfile() throws IOException {
|
||||
public void testUpdateProfileSimple() throws IOException {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
@ -148,6 +213,21 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
|
|||
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
|
||||
}
|
||||
|
||||
private UserRepresentation getProfile() throws IOException {
|
||||
return SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
}
|
||||
|
||||
private void updateProfileExpectSuccess(UserRepresentation user) throws IOException {
|
||||
int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus();
|
||||
assertEquals(204, status);
|
||||
}
|
||||
|
||||
private void updateProfileExpectError(UserRepresentation user, int expectedStatus, String expectedMessage) throws IOException {
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
assertEquals(expectedStatus, response.getStatus());
|
||||
assertEquals(expectedMessage, response.asJson(ErrorRepresentation.class).getErrorMessage());
|
||||
}
|
||||
|
||||
// Send REST request to get all credential containers and credentials of current user
|
||||
private List<AccountCredentialResource.CredentialContainer> getCredentials() throws IOException {
|
||||
return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient)
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright 2020 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.testsuite.federation.ldap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.federation.kerberos.KerberosFederationProvider;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class LDAPAdminRestApiTest extends AbstractLDAPTest {
|
||||
|
||||
@ClassRule
|
||||
public static LDAPRule ldapRule = new LDAPRule();
|
||||
|
||||
@Override
|
||||
protected LDAPRule getLDAPRule() {
|
||||
return ldapRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterImportTestRealm() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
LDAPTestUtils.addLocalUser(session, appRealm, "marykeycloak", "mary@test.com", "password-app");
|
||||
|
||||
LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ctx.getLdapModel());
|
||||
|
||||
// Delete all LDAP users and add some new for testing
|
||||
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
|
||||
|
||||
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
|
||||
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUserWithAdminRest() throws Exception {
|
||||
// Create user just with the username
|
||||
UserRepresentation user1 = UserBuilder.create()
|
||||
.username("admintestuser1")
|
||||
.password("userpass")
|
||||
.enabled(true)
|
||||
.build();
|
||||
String newUserId1 = createUserExpectSuccess(user1);
|
||||
getCleanup().addUserId(newUserId1);
|
||||
|
||||
// Create user with firstName and lastNAme
|
||||
UserRepresentation user2 = UserBuilder.create()
|
||||
.username("admintestuser2")
|
||||
.password("userpass")
|
||||
.email("admintestuser2@keycloak.org")
|
||||
.firstName("Some")
|
||||
.lastName("OtherUser")
|
||||
.enabled(true)
|
||||
.build();
|
||||
String newUserId2 = createUserExpectSuccess(user2);
|
||||
getCleanup().addUserId(newUserId2);
|
||||
|
||||
// Create user with filled LDAP_ID should fail
|
||||
UserRepresentation user3 = UserBuilder.create()
|
||||
.username("admintestuser3")
|
||||
.password("userpass")
|
||||
.addAttribute(LDAPConstants.LDAP_ID, "123456")
|
||||
.enabled(true)
|
||||
.build();
|
||||
createUserExpectError(user3);
|
||||
|
||||
// Create user with filled LDAP_ENTRY_DN should fail
|
||||
UserRepresentation user4 = UserBuilder.create()
|
||||
.username("admintestuser4")
|
||||
.password("userpass")
|
||||
.addAttribute(LDAPConstants.LDAP_ENTRY_DN, "ou=users,dc=foo")
|
||||
.enabled(true)
|
||||
.build();
|
||||
createUserExpectError(user4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateUserWithAdminRest() throws Exception {
|
||||
UserResource userRes = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak");
|
||||
UserRepresentation user = userRes.toRepresentation();
|
||||
|
||||
List<String> origLdapId = new ArrayList<>(user.getAttributes().get(LDAPConstants.LDAP_ID));
|
||||
List<String> origLdapEntryDn = new ArrayList<>(user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN));
|
||||
Assert.assertEquals(1, origLdapId.size());
|
||||
Assert.assertEquals(1, origLdapEntryDn.size());
|
||||
Assert.assertThat(user.getAttributes().keySet(), not(contains(KerberosFederationProvider.KERBEROS_PRINCIPAL)));
|
||||
|
||||
// Trying to add KERBEROS_PRINCIPAL should fail (Adding attribute, which was not yet present)
|
||||
user.setFirstName("JohnUpdated");
|
||||
user.setLastName("DoeUpdated");
|
||||
user.singleAttribute(KerberosFederationProvider.KERBEROS_PRINCIPAL, "foo");
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
// The same test, but consider case sensitivity
|
||||
user.getAttributes().remove(KerberosFederationProvider.KERBEROS_PRINCIPAL);
|
||||
user.singleAttribute("KERberos_principal", "foo");
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
// Trying to update LDAP_ID should fail (Updating existing attribute, which was present)
|
||||
user.getAttributes().remove("KERberos_principal");
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).remove(0);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).add("123");
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
// Trying to delete LDAP_ID should fail (Removing attribute, which was present here already)
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ID).remove(0);
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
user.getAttributes().remove(LDAPConstants.LDAP_ID);
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
// Trying to update LDAP_ENTRY_DN should fail
|
||||
user.getAttributes().put(LDAPConstants.LDAP_ID, origLdapId);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN).remove(0);
|
||||
user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN).add("ou=foo,dc=bar");
|
||||
updateUserExpectError(userRes, user);
|
||||
|
||||
// Update firstName and lastName should be fine
|
||||
user.getAttributes().put(LDAPConstants.LDAP_ENTRY_DN, origLdapEntryDn);
|
||||
userRes.update(user);
|
||||
|
||||
user = userRes.toRepresentation();
|
||||
assertEquals("JohnUpdated", user.getFirstName());
|
||||
assertEquals("DoeUpdated", user.getLastName());
|
||||
assertEquals(origLdapId, user.getAttributes().get(LDAPConstants.LDAP_ID));
|
||||
assertEquals(origLdapEntryDn, user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN));
|
||||
|
||||
// Revert
|
||||
user.setFirstName("John");
|
||||
user.setLastName("Doe");
|
||||
userRes.update(user);
|
||||
}
|
||||
|
||||
|
||||
private String createUserExpectSuccess(UserRepresentation user) {
|
||||
Response response = testRealm().users().create(user);
|
||||
String newUserId = ApiUtil.getCreatedId(response);
|
||||
response.close();
|
||||
|
||||
UserRepresentation userRep = testRealm().users().get(newUserId).toRepresentation();
|
||||
userRep.getAttributes().containsKey(LDAPConstants.LDAP_ID);
|
||||
userRep.getAttributes().containsKey(LDAPConstants.LDAP_ENTRY_DN);
|
||||
return newUserId;
|
||||
}
|
||||
|
||||
private void createUserExpectError(UserRepresentation user) {
|
||||
Response response = testRealm().users().create(user);
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
response.close();
|
||||
}
|
||||
|
||||
private void updateUserExpectError(UserResource userRes, UserRepresentation user) {
|
||||
try {
|
||||
userRes.update(user);
|
||||
Assert.fail("Not expected to successfully update user");
|
||||
} catch (BadRequestException e) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
}
|
|
@ -194,6 +194,13 @@
|
|||
}
|
||||
},
|
||||
|
||||
"userProfile": {
|
||||
"legacy-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
}
|
||||
},
|
||||
|
||||
"x509cert-lookup": {
|
||||
"provider": "${keycloak.x509cert.lookup.provider:default}",
|
||||
"default": {
|
||||
|
|
|
@ -112,6 +112,13 @@
|
|||
}
|
||||
},
|
||||
|
||||
"userProfile": {
|
||||
"legacy-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
}
|
||||
},
|
||||
|
||||
"x509cert-lookup": {
|
||||
"provider": "${keycloak.x509cert.lookup.provider:}",
|
||||
"haproxy": {
|
||||
|
|
|
@ -171,6 +171,7 @@ missingEmailMessage=Please specify email.
|
|||
missingPasswordMessage=Please specify password.
|
||||
notMatchPasswordMessage=Passwords don''t match.
|
||||
invalidUserMessage=Invalid user
|
||||
updateReadOnlyAttributesRejectedMessage=Update of read-only attribute rejected
|
||||
|
||||
missingTotpMessage=Please specify authenticator code.
|
||||
missingTotpDeviceNameMessage=Please specify device name.
|
||||
|
|
Loading…
Reference in a new issue