[KEYCLOAK-17123] - Avoid validation and updates for read-only attributes during updates

This commit is contained in:
Pedro Igor 2021-02-12 21:54:15 -03:00 committed by Marek Posolda
parent 8f719885fd
commit 431f137c37
26 changed files with 358 additions and 159 deletions

View file

@ -24,10 +24,18 @@ import java.util.List;
public class UserProfileAttributes extends HashMap<String, List<String>> {
public UserProfileAttributes(Map<String, List<String>> attribtues){
private final UserProfileProvider profileProvider;
public UserProfileAttributes(Map<String, List<String>> attribtues,
UserProfileProvider profileProvider){
this.profileProvider = profileProvider;
this.putAll(attribtues);
}
public UserProfileAttributes(Map<String, List<String>> attribtues){
this(attribtues, null);
}
public void setAttribute(String key, List<String> value){
this.put(key,value);
}
@ -47,4 +55,8 @@ public class UserProfileAttributes extends HashMap<String, List<String>> {
public void removeAttribute(String attr) {
this.remove(attr);
}
public boolean isReadOnlyAttribute(String key) {
return profileProvider != null && profileProvider.isReadOnlyAttribute(key);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.userprofile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
/**
@ -26,4 +27,5 @@ public interface UserProfileContext {
UserUpdateEvent getUpdateEvent();
UserProfile getCurrentProfile();
UserProfileValidationResult validate();
}

View file

@ -26,4 +26,6 @@ import org.keycloak.userprofile.validation.UserProfileValidationResult;
public interface UserProfileProvider extends Provider {
UserProfileValidationResult validate(UserProfileContext updateContext, UserProfile updatedProfile);
boolean isReadOnlyAttribute(String key);
}

View file

@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.keycloak.userprofile.UserProfile;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
@ -28,9 +30,12 @@ public class UserProfileValidationResult {
List<AttributeValidationResult> attributeValidationResults;
private final UserProfile updatedProfile;
public UserProfileValidationResult(List<AttributeValidationResult> attributeValidationResults) {
public UserProfileValidationResult(List<AttributeValidationResult> attributeValidationResults,
UserProfile updatedProfile) {
this.attributeValidationResults = attributeValidationResults;
this.updatedProfile = updatedProfile;
}
public List<AttributeValidationResult> getValidationResults() {
@ -50,4 +55,13 @@ public class UserProfileValidationResult {
public boolean hasAttributeChanged(String attribute) {
return this.attributeValidationResults.stream().filter(o -> o.getField().equals(attribute)).collect(Collectors.toList()).get(0).hasChanged();
}
/**
* Returns the {@link UserProfile} used during validations.
*
* @return the profile user during validations
*/
public UserProfile getProfile() {
return updatedProfile;
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.authentication.authenticators.broker;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forIdpReview;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
@ -33,12 +35,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
@ -104,14 +102,8 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
EventBuilder event = context.getEvent();
event.event(EventType.UPDATE_PROFILE);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
UserProfileValidationResult result = forIdpReview(userCtx, formData, context.getSession()).validate();
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = userCtx.getEmail();
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
UserProfileValidationResult result = profileProvider.validate(DefaultUserProfileContext.forIdpReview(userCtx), updatedProfile);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (errors != null && !errors.isEmpty()) {
@ -124,6 +116,8 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
return;
}
UserProfile updatedProfile = result.getProfile();
UserUpdateHelper.updateIdpReview(context.getRealm(), new UserModelDelegate(null) {
@Override
public Map<String, List<String>> getAttributes() {
@ -150,6 +144,9 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
String oldEmail = userCtx.getEmail();
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
if (result.hasAttributeChanged(UserModel.EMAIL)) {
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail).success();

View file

@ -17,6 +17,8 @@
package org.keycloak.authentication.forms;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationProfile;
import org.keycloak.Config;
import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory;
@ -34,12 +36,9 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import javax.ws.rs.core.MultivaluedMap;
@ -65,18 +64,17 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
@Override
public void validate(org.keycloak.authentication.ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
UserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
UserProfileProvider userProfile = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfileValidationResult result = userProfile.validate(DefaultUserProfileContext.forRegistrationProfile(), updatedProfile);
UserProfileValidationResult result = forRegistrationProfile(context.getSession(), formData).validate();
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (errors.size() > 0) {
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL))
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
UserProfile updatedProfile = result.getProfile();
context.getEvent().detail(Details.EMAIL, updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL));
}
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS)) {
context.error(Errors.EMAIL_IN_USE);

View file

@ -17,6 +17,8 @@
package org.keycloak.authentication.forms;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationUserCreation;
import org.keycloak.Config;
import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory;
@ -37,12 +39,9 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import javax.ws.rs.core.MultivaluedMap;
@ -71,17 +70,17 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfile newProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
UserProfileValidationResult result = forRegistrationUserCreation(context.getSession(), formData).validate();
UserProfile newProfile = result.getProfile();
String email = newProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
String username = newProfile.getAttributes().getFirstAttribute(UserModel.USERNAME);
context.getEvent().detail(Details.EMAIL, email);
String username = newProfile.getAttributes().getFirstAttribute(UserModel.USERNAME);
context.getEvent().detail(Details.USERNAME, username);
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfileValidationResult result = profileProvider.validate(DefaultUserProfileContext.forRegistrationUserCreation(), newProfile);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (context.getRealm().isRegistrationEmailAsUsername()) {
context.getEvent().detail(Details.USERNAME, email);

View file

@ -17,6 +17,8 @@
package org.keycloak.authentication.requiredactions;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forUpdateProfile;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
@ -31,13 +33,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import javax.ws.rs.core.MultivaluedMap;
@ -71,14 +69,9 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
event.event(EventType.UPDATE_PROFILE);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
UserModel user = context.getUser();
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = user.getEmail();
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
UserProfileProvider userProfile = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
UserProfileValidationResult result = userProfile.validate(DefaultUserProfileContext.forUpdateProfile(user),updatedProfile);
UserProfileValidationResult result = forUpdateProfile(user, formData, context.getSession()).validate();
final UserProfile updatedProfile = result.getProfile();
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (errors != null && !errors.isEmpty()) {
@ -90,6 +83,8 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
return;
}
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
UserUpdateHelper.updateUserProfile(context.getRealm(), user, updatedProfile);
if (result.hasAttributeChanged(UserModel.EMAIL)) {
user.setEmailVerified(false);

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.services.resources.account;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forOldAccount;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.PermissionTicket;
@ -66,17 +68,13 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AbstractSecuredLocalService;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.CredentialHelper;
@ -365,15 +363,11 @@ public class AccountFormService extends AbstractSecuredLocalService {
csrfCheck(formData);
UserModel user = auth.getUser();
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = user.getEmail();
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
UserProfileValidationResult result = profileProvider.validate(DefaultUserProfileContext.forAccountService(user), updatedProfile);
UserProfileValidationResult result = forOldAccount(user, formData, session).validate();
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (!errors.isEmpty()) {
@ -389,6 +383,9 @@ public class AccountFormService extends AbstractSecuredLocalService {
return account.setErrors(status, errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
}
UserProfile updatedProfile = result.getProfile();
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
try {
// backward compatibility with old account console where attributes are not removed if missing
UserUpdateHelper.updateAccountOldConsole(realm, user, updatedProfile);

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.services.resources.account;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forAccountService;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection;
@ -45,11 +47,6 @@ import org.keycloak.services.resources.account.resources.ResourcesService;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.theme.Theme;
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.validation.UserProfileValidationResult;
@ -160,9 +157,7 @@ public class AccountRestService {
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
UserProfile updatedUser = new AccountUserRepresentationUserProfile(rep);
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
UserProfileValidationResult result = profileProvider.validate(DefaultUserProfileContext.forAccountService(user), updatedUser);
UserProfileValidationResult result = forAccountService(user, rep, session).validate();
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME))
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
@ -177,7 +172,7 @@ public class AccountRestService {
}
try {
UserUpdateHelper.updateAccount(realm, user, updatedUser);
UserUpdateHelper.updateAccount(realm, user, result.getProfile());
event.success();
return Response.noContent().build();

View file

@ -71,13 +71,7 @@ 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;
@ -119,6 +113,7 @@ import java.util.stream.Stream;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forUserResource;
/**
* Base resource for managing users
@ -201,9 +196,7 @@ public class UserResource {
}
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);
UserProfileValidationResult result = forUserResource(user, rep, session).validate();
if (!result.getErrors().isEmpty()) {
for (AttributeValidationResult attrValidation : result.getErrors()) {
StringBuilder s = new StringBuilder("Failed to update attribute " + attrValidation.getField() + ": ");
@ -220,7 +213,7 @@ public class UserResource {
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);
UserUpdateHelper.updateUserResource(session, user, rep, isUpdateExistingUser);
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());

View file

@ -1,28 +1,26 @@
/*
* 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
* * Copyright 2021 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.
*
* 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.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;
import org.keycloak.models.UserModel;
@ -36,7 +34,6 @@ import org.keycloak.userprofile.validation.ValidationChainBuilder;
*/
public class LegacyUserProfileProvider implements UserProfileProvider {
private static final Logger logger = Logger.getLogger(LegacyUserProfileProvider.class);
private final KeycloakSession session;
private final Pattern readOnlyAttributes;
private final Pattern adminReadOnlyAttributes;
@ -77,7 +74,12 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
break;
}
return new UserProfileValidationResult(builder.build().validate(updateContext,updatedProfile));
return new UserProfileValidationResult(builder.build().validate(updateContext,updatedProfile), updatedProfile);
}
@Override
public boolean isReadOnlyAttribute(String key) {
return readOnlyAttributes.matcher(key).find() || adminReadOnlyAttributes.matcher(key).find();
}
private void addUserCreationValidators(ValidationChainBuilder builder) {
@ -130,12 +132,12 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
}
private void addReadOnlyAttributeValidators(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrs, UserProfileContext updateContext, UserProfile updatedProfile) {
addValidatorsForAllAttributeOfUser(builder, configuredReadOnlyAttrs, updatedProfile);
addValidatorsForAllAttributeOfUser(builder, configuredReadOnlyAttrs, updateContext.getCurrentProfile());
addValidatorsForReadOnlyAttributes(builder, configuredReadOnlyAttrs, updatedProfile);
addValidatorsForReadOnlyAttributes(builder, configuredReadOnlyAttrs, updateContext.getCurrentProfile());
}
private void addValidatorsForAllAttributeOfUser(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrsPattern, UserProfile profile) {
private void addValidatorsForReadOnlyAttributes(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrsPattern, UserProfile profile) {
if (profile == null) {
return;
}
@ -144,7 +146,7 @@ public class LegacyUserProfileProvider implements UserProfileProvider {
.filter(currentAttrName -> configuredReadOnlyAttrsPattern.matcher(currentAttrName).find())
.forEach((currentAttrName) ->
builder.addAttributeValidator().forAttribute(currentAttrName)
.addValidationFunction(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, StaticValidators.isAttributeUnchanged(currentAttrName)).build()
.addValidationFunction(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, StaticValidators.isReadOnlyAttributeUnchanged(currentAttrName)).build()
);
}
}

View file

@ -1,7 +1,25 @@
/*
* 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.userprofile.profile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileAttributes;
import org.keycloak.userprofile.UserProfileProvider;
import java.util.List;
import java.util.Map;
@ -11,15 +29,12 @@ public abstract class AbstractUserProfile implements UserProfile {
private final UserProfileAttributes attributes;
public AbstractUserProfile(Map<String, List<String>> attributes) {
this.attributes = new UserProfileAttributes(attributes);
public AbstractUserProfile(Map<String, List<String>> attributes, UserProfileProvider profileProvider) {
this.attributes = new UserProfileAttributes(attributes, profileProvider);
}
@Override
public UserProfileAttributes getAttributes() {
return this.attributes;
}
}

View file

@ -17,15 +17,10 @@
package org.keycloak.userprofile.profile;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileAttributes;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.profile.representations.IdpUserProfile;
import org.keycloak.userprofile.profile.representations.UserModelUserProfile;
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
/**
@ -33,40 +28,17 @@ import org.keycloak.userprofile.validation.UserUpdateEvent;
*/
public class DefaultUserProfileContext implements UserProfileContext {
private UserProfile currentUserProfile;
private final UserProfile updatedProfile;
private final UserProfileProvider profileProvider;
private UserUpdateEvent userUpdateEvent;
private DefaultUserProfileContext(UserUpdateEvent userUpdateEvent, UserProfile currentUserProfile) {
DefaultUserProfileContext(UserUpdateEvent userUpdateEvent, UserProfile currentUserProfile,
UserProfile updatedProfile,
UserProfileProvider profileProvider) {
this.userUpdateEvent = userUpdateEvent;
this.currentUserProfile = currentUserProfile;
}
public static DefaultUserProfileContext forIdpReview(SerializedBrokeredIdentityContext currentUser) {
return new DefaultUserProfileContext(UserUpdateEvent.IdpReview, new IdpUserProfile(currentUser));
}
public static DefaultUserProfileContext forUpdateProfile(UserModel currentUser) {
return new DefaultUserProfileContext(UserUpdateEvent.UpdateProfile, new UserModelUserProfile(currentUser));
}
public static DefaultUserProfileContext forAccountService(UserModel currentUser) {
return new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(currentUser));
}
public static DefaultUserProfileContext forRegistrationUserCreation() {
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationUserCreation, null);
}
public static DefaultUserProfileContext forRegistrationProfile() {
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, null);
}
/**
* @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);
this.updatedProfile = updatedProfile;
this.profileProvider = profileProvider;
}
@Override
@ -78,4 +50,9 @@ public class DefaultUserProfileContext implements UserProfileContext {
public UserUpdateEvent getUpdateEvent(){
return userUpdateEvent;
}
@Override
public UserProfileValidationResult validate() {
return profileProvider.validate(this, updatedProfile);
}
}

View file

@ -0,0 +1,109 @@
/*
* Copyright 2021 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.userprofile.profile;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.representations.account.UserRepresentation;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
import org.keycloak.userprofile.profile.representations.IdpUserProfile;
import org.keycloak.userprofile.profile.representations.UserModelUserProfile;
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
import org.keycloak.userprofile.validation.UserUpdateEvent;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public final class UserProfileContextFactory {
public static DefaultUserProfileContext forIdpReview(SerializedBrokeredIdentityContext currentUser,
MultivaluedMap<String, String> formData, KeycloakSession session) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.IdpReview, new IdpUserProfile(currentUser, profileProvider),
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
}
public static DefaultUserProfileContext forUpdateProfile(UserModel currentUser,
MultivaluedMap<String, String> formData,
KeycloakSession session) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.UpdateProfile, new UserModelUserProfile(currentUser, profileProvider),
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
}
public static DefaultUserProfileContext forAccountService(UserModel currentUser,
UserRepresentation rep, KeycloakSession session) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(currentUser, profileProvider),
new AccountUserRepresentationUserProfile(rep, profileProvider),
profileProvider);
}
public static DefaultUserProfileContext forOldAccount(UserModel currentUser,
MultivaluedMap<String, String> formData, KeycloakSession session) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(currentUser, profileProvider),
AttributeFormDataProcessor.toUserProfile(formData),
profileProvider);
}
public static DefaultUserProfileContext forRegistrationUserCreation(
KeycloakSession session, MultivaluedMap<String, String> formData) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationUserCreation, null,
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
}
public static DefaultUserProfileContext forRegistrationProfile(KeycloakSession session,
MultivaluedMap<String, String> formData) {
UserProfileProvider profileProvider = getProfileProvider(session);
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, null,
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
}
/**
* @param currentUser if this is null, then we're creating new user. If it is not null, we're updating existing user
* @param rep
* @return user profile context for the validation of user when called from admin REST API
*/
public static DefaultUserProfileContext forUserResource(UserModel currentUser,
org.keycloak.representations.idm.UserRepresentation rep, KeycloakSession session) {
UserProfileProvider profileProvider = getProfileProvider(session);
UserProfile currentUserProfile = currentUser == null ? null : new UserModelUserProfile(currentUser, profileProvider);
return new DefaultUserProfileContext(UserUpdateEvent.UserResource, currentUserProfile,
new UserRepresentationUserProfile(rep, profileProvider), profileProvider);
}
public static DefaultUserProfileContext forProfile(UserUpdateEvent event) {
return new DefaultUserProfileContext(event, null, null, null);
}
private static UserProfileProvider getProfileProvider(KeycloakSession session) {
if (session == null) {
return null;
}
return session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
}
}

View file

@ -21,6 +21,7 @@ package org.keycloak.userprofile.profile.representations;
import org.keycloak.models.UserModel;
import org.keycloak.representations.account.UserRepresentation;
import org.keycloak.userprofile.UserProfileAttributes;
import org.keycloak.userprofile.UserProfileProvider;
import java.util.Collections;
import java.util.HashMap;
@ -32,8 +33,8 @@ import java.util.Map;
*/
public class AccountUserRepresentationUserProfile extends AttributeUserProfile {
public AccountUserRepresentationUserProfile(UserRepresentation user) {
super(flattenUserRepresentation(user));
public AccountUserRepresentationUserProfile(UserRepresentation user, UserProfileProvider profileProvider) {
super(flattenUserRepresentation(user), profileProvider);
}
private static UserProfileAttributes flattenUserRepresentation(UserRepresentation user) {

View file

@ -17,6 +17,7 @@
package org.keycloak.userprofile.profile.representations;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.AbstractUserProfile;
import javax.ws.rs.NotSupportedException;
@ -28,8 +29,12 @@ import java.util.Map;
*/
public class AttributeUserProfile extends AbstractUserProfile {
public AttributeUserProfile(Map<String, List<String>> attributes, UserProfileProvider profileProvider) {
super(attributes, profileProvider);
}
public AttributeUserProfile(Map<String, List<String>> attributes) {
super(attributes);
super(attributes, null);
}
@Override

View file

@ -18,6 +18,7 @@
package org.keycloak.userprofile.profile.representations;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.AbstractUserProfile;
@ -28,8 +29,8 @@ public class IdpUserProfile extends AbstractUserProfile {
private final SerializedBrokeredIdentityContext user;
public IdpUserProfile(SerializedBrokeredIdentityContext user) {
super(user.getAttributes());
public IdpUserProfile(SerializedBrokeredIdentityContext user, UserProfileProvider profileProvider) {
super(user.getAttributes(), profileProvider);
this.user = user;
}

View file

@ -18,6 +18,7 @@
package org.keycloak.userprofile.profile.representations;
import org.keycloak.models.UserModel;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.AbstractUserProfile;
/**
@ -26,8 +27,8 @@ import org.keycloak.userprofile.profile.AbstractUserProfile;
public class UserModelUserProfile extends AbstractUserProfile {
public UserModelUserProfile(UserModel user) {
super(user.getAttributes());
public UserModelUserProfile(UserModel user, UserProfileProvider profileProvider) {
super(user.getAttributes(), profileProvider);
this.user = user;
}

View file

@ -20,6 +20,7 @@ package org.keycloak.userprofile.profile.representations;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.userprofile.UserProfileAttributes;
import org.keycloak.userprofile.UserProfileProvider;
import java.util.Collections;
import java.util.HashMap;
@ -32,8 +33,12 @@ import java.util.Map;
public class UserRepresentationUserProfile extends AttributeUserProfile {
public UserRepresentationUserProfile(UserRepresentation user, UserProfileProvider profileProvider) {
super(flattenUserRepresentation(user), profileProvider);
}
public UserRepresentationUserProfile(UserRepresentation user) {
super(flattenUserRepresentation(user));
super(flattenUserRepresentation(user), null);
}
private static UserProfileAttributes flattenUserRepresentation(UserRepresentation user) {

View file

@ -17,11 +17,16 @@
package org.keycloak.userprofile.utils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileAttributes;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import java.util.Collections;
@ -71,8 +76,11 @@ public class UserUpdateHelper {
update(UserUpdateEvent.Account, realm, user, updatedProfile.getAttributes(), false);
}
public static void updateUserResource(RealmModel realm, UserModel user, UserProfile userRepresentationUserProfile, boolean removeExistingAttributes) {
update(UserUpdateEvent.UserResource, realm, user, userRepresentationUserProfile.getAttributes(), removeExistingAttributes);
public static void updateUserResource(KeycloakSession session, UserModel user, UserRepresentation rep, boolean removeExistingAttributes) {
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
RealmModel realm = session.getContext().getRealm();
UserRepresentationUserProfile userProfile = new UserRepresentationUserProfile(rep, profileProvider);
update(UserUpdateEvent.UserResource, realm, user, userProfile.getAttributes(), removeExistingAttributes);
}
/**
@ -128,8 +136,8 @@ public class UserUpdateHelper {
}
}
private static void updateAttributes(UserModel currentUser, Map<String, List<String>> updatedUser, boolean removeMissingAttributes) {
for (Map.Entry<String, List<String>> attr : updatedUser.entrySet()) {
private static void updateAttributes(UserModel currentUser, UserProfileAttributes attributes, boolean removeMissingAttributes) {
for (Map.Entry<String, List<String>> attr : attributes.entrySet()) {
List<String> currentValue = currentUser.getAttributeStream(attr.getKey()).collect(Collectors.toList());
//In case of username we need to provide lower case values
List<String> updatedValue = attr.getKey().equals(UserModel.USERNAME) ? AttributeToLower(attr.getValue()) : attr.getValue();
@ -139,9 +147,12 @@ public class UserUpdateHelper {
}
if (removeMissingAttributes) {
Set<String> attrsToRemove = new HashSet<>(currentUser.getAttributes().keySet());
attrsToRemove.removeAll(updatedUser.keySet());
attrsToRemove.removeAll(attributes.keySet());
for (String attr : attrsToRemove) {
if (attributes.isReadOnlyAttribute(attr)) {
continue;
}
currentUser.removeAttribute(attr);
}

View file

@ -107,10 +107,14 @@ public class StaticValidators {
&& session.users().getUserByEmail(session.getContext().getRealm(), value) != null);
}
public static BiFunction<List<String>, UserProfileContext, Boolean> isAttributeUnchanged(String attributeName) {
public static BiFunction<List<String>, UserProfileContext, Boolean> isReadOnlyAttributeUnchanged(String attributeName) {
return (newAttrValues, context) -> {
if (newAttrValues == null) {
return true;
}
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));
}

View file

@ -1,5 +1,24 @@
/*
* 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.userprofile.validation;
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forProfile;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -35,14 +54,14 @@ public class ValidationChainTest {
rep.singleAttribute("FAKE_FIELD", "content");
rep.singleAttribute("NULLABLE_FIELD", null);
updateContext = DefaultUserProfileContext.forRegistrationProfile();
updateContext = forProfile(UserUpdateEvent.RegistrationProfile);
}
@Test
public void validate() {
testchain = builder.build();
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)));
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)), null);
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY"));
Assert.assertEquals(false, results.hasFailureOfErrorType("FIRST_NAME_FIELD_ERRORKEY"));
Assert.assertEquals(true, results.getValidationResults().stream().filter(o -> o.getField().equals("firstName")).collect(Collectors.toList()).get(0).isValid());
@ -57,7 +76,7 @@ public class ValidationChainTest {
.addAttributeValidator().forAttribute("FAKE_FIELD")
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY_2", (value, updateUserProfileContext) -> false).build().build();
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)));
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)), null);
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());
@ -67,7 +86,7 @@ public class ValidationChainTest {
@Test
public void emptyChain() {
UserProfileValidationResult results = new UserProfileValidationResult(ValidationChainBuilder.builder().build().validate(updateContext,new UserRepresentationUserProfile(rep) ));
UserProfileValidationResult results = new UserProfileValidationResult(ValidationChainBuilder.builder().build().validate(updateContext,new UserRepresentationUserProfile(rep) ), null);
Assert.assertEquals(Collections.emptyList(), results.getValidationResults());
}
}

View file

@ -36,6 +36,8 @@ 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.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
@ -135,9 +137,10 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
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)
// Ignore removal of read-only attributes
user.getAttributes().remove(attrName);
updateError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
user = updateAndGet(user);
assertTrue(user.getAttributes().containsKey(attrName));
// Revert with admin REST
adminUserRep.getAttributes().remove(attrName);

View file

@ -52,6 +52,7 @@ import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -142,8 +143,11 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
user.getAttributes().get(LDAPConstants.LDAP_ID).remove(0);
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
// ignore removal for read-only attributes
user.getAttributes().remove(LDAPConstants.LDAP_ID);
updateProfileExpectError(user, 400, Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED);
updateProfileExpectSuccess(user);
user = getProfile();
assertFalse(user.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
// Trying to update LDAP_ENTRY_DN should fail
user.getAttributes().put(LDAPConstants.LDAP_ID, origLdapId);
@ -208,6 +212,42 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
}
@Test
public void testIgnoreReadOnlyAttributes() throws IOException {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
appRealm.setEditUsernameAllowed(false);
});
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
user.setEmail("john-alias@email.org");
SimpleHttp.doPost(getAccountUrl(null), httpClient).json(user).auth(tokenUtil.getToken()).asStatus();
UserRepresentation usernew = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
assertEquals("johnkeycloak", usernew.getUsername());
assertEquals("John", usernew.getFirstName());
assertEquals("Doe", usernew.getLastName());
assertEquals("john-alias@email.org", usernew.getEmail());
assertFalse(usernew.isEmailVerified());
usernew.getAttributes().clear();
//clean up
usernew.setEmail("john@email.org");
final int i = SimpleHttp.doPost(getAccountUrl(null), httpClient).json(usernew).auth(tokenUtil.getToken()).asStatus();
org.keycloak.representations.idm.UserRepresentation userRep = testRealm().users()
.search(usernew.getUsername()).get(0);
userRep.setAttributes(null);
testRealm().users().get(userRep.getId()).update(userRep);
usernew = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
assertTrue(usernew.getAttributes().containsKey(LDAPConstants.LDAP_ID));
assertTrue(usernew.getAttributes().containsKey(LDAPConstants.LDAP_ENTRY_DN));
}
private String getAccountUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");

View file

@ -152,7 +152,9 @@ public class LDAPAdminRestApiTest extends AbstractLDAPTest {
updateUserExpectError(userRes, user);
user.getAttributes().remove(LDAPConstants.LDAP_ID);
updateUserExpectError(userRes, user);
userRes.update(user);
user = userRes.toRepresentation();
assertEquals(origLdapId, user.getAttributes().get(LDAPConstants.LDAP_ID));
// Trying to update LDAP_ENTRY_DN should fail
user.getAttributes().put(LDAPConstants.LDAP_ID, origLdapId);