UserProfile M1

This commit is contained in:
Markus Till 2020-10-01 12:34:35 +02:00 committed by Pedro Igor
parent efa16b5ac4
commit 72f73f153a
44 changed files with 1916 additions and 391 deletions

View file

@ -20,6 +20,9 @@ package org.keycloak.representations.account;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.keycloak.json.StringListMapDeserializer; import org.keycloak.json.StringListMapDeserializer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -94,4 +97,13 @@ public class UserRepresentation {
this.attributes = attributes; this.attributes = attributes;
} }
public void singleAttribute(String name, String value) {
if (this.attributes == null) this.attributes=new HashMap<>();
attributes.put(name, (value == null ? new ArrayList<String>() : Arrays.asList(value)));
}
public String firstAttribute(String key) {
return this.attributes == null ? null : this.attributes.containsKey(key) ? this.attributes.get(key).get(0) : null;
}
} }

View file

@ -157,11 +157,15 @@ public class UserRepresentation {
} }
public UserRepresentation singleAttribute(String name, String value) { public UserRepresentation singleAttribute(String name, String value) {
if (this.attributes == null) attributes = new HashMap<>(); if (this.attributes == null) this.attributes=new HashMap<>();
attributes.put(name, (value == null ? new ArrayList<String>() : Arrays.asList(value))); attributes.put(name, (value == null ? new ArrayList<String>() : Arrays.asList(value)));
return this; return this;
} }
public String firstAttribute(String key) {
return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0);
}
public List<CredentialRepresentation> getCredentials() { public List<CredentialRepresentation> getCredentials() {
return credentials; return credentials;
} }

View file

@ -178,7 +178,7 @@ public class UserAdapter implements CachedUserModel {
getDelegateForUpdate(); getDelegateForUpdate();
if (UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name)) { if (UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name)) {
String lowerCasedFirstValue = KeycloakModelUtils.toLowerCaseSafe((values != null && values.size() > 0) ? values.get(0) : null); String lowerCasedFirstValue = KeycloakModelUtils.toLowerCaseSafe((values != null && values.size() > 0) ? values.get(0) : null);
if (lowerCasedFirstValue != null) values.set(0, lowerCasedFirstValue); if (lowerCasedFirstValue != null) values=Collections.singletonList(lowerCasedFirstValue);
} }
updated.setAttribute(name, values); updated.setAttribute(name, values);
} }

View file

@ -0,0 +1,39 @@
/*
* 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;
import java.util.List;
import java.util.Map;
/**
* Abstraction, which allows to update the user in various contexts (Required action of already existing user, or first identity provider
* login when user doesn't yet exists in Keycloak DB)
*
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface UserProfile {
String getId();
Map<String, List<String>> getAttributes();
String getFirstAttribute(String name);
List<String> getAttribute(String key);
}

View file

@ -0,0 +1,31 @@
/*
* 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;
import org.keycloak.userprofile.validation.UserUpdateEvent;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface UserProfileContext {
boolean isCreate();
UserUpdateEvent getUpdateEvent();
UserProfile getCurrent();
UserProfile getUpdated();
}

View file

@ -0,0 +1,30 @@
/*
* 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;
import org.keycloak.provider.Provider;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface UserProfileProvider extends Provider {
UserProfileValidationResult validate(UserProfileContext updateContext);
}

View file

@ -0,0 +1,27 @@
/*
* 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;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface UserProfileProviderFactory extends ProviderFactory<UserProfileProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* 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;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class UserProfileSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "userProfile";
}
@Override
public Class<? extends Provider> getProviderClass() {
return UserProfileProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return UserProfileProviderFactory.class;
}
}

View file

@ -0,0 +1,71 @@
/*
* 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 java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class AttributeValidationResult {
private final String attributeKey;
private final boolean changed;
List<ValidationResult> validationResults;
public List<ValidationResult> getValidationResults() {
return validationResults;
}
public List<ValidationResult> getFailedValidations() {
return validationResults == null ? null : validationResults.stream().filter(ValidationResult::isInvalid).collect(Collectors.toList());
}
public AttributeValidationResult(String attributeKey, boolean changed, List<ValidationResult> validationResults) {
this.attributeKey = attributeKey;
this.validationResults = validationResults;
this.changed = changed;
}
public boolean isValid() {
return validationResults.stream().allMatch(ValidationResult::isValid);
}
protected boolean isInvalid() {
return !isValid();
}
public boolean hasChanged() {
return changed;
}
public String getField() {
return attributeKey;
}
public boolean hasFailureOfErrorType(String... errorKeys) {
return this.validationResults != null
&& this.getFailedValidations().stream().anyMatch(o -> o.getErrorType() != null
&& Arrays.stream(errorKeys).anyMatch(a -> a.equals(o.getErrorType())));
}
}

View file

@ -0,0 +1,53 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class UserProfileValidationResult {
List<AttributeValidationResult> attributeValidationResults;
public UserProfileValidationResult(List<AttributeValidationResult> attributeValidationResults) {
this.attributeValidationResults = attributeValidationResults;
}
public List<AttributeValidationResult> getValidationResults() {
return attributeValidationResults;
}
public List<AttributeValidationResult> getErrors() {
return attributeValidationResults.stream().filter(AttributeValidationResult::isInvalid).collect(Collectors.toCollection(ArrayList::new));
}
public boolean hasFailureOfErrorType(String... errorKeys) {
return this.attributeValidationResults != null
&& this.attributeValidationResults.stream().anyMatch(attributeValidationResult -> attributeValidationResult.hasFailureOfErrorType(errorKeys));
}
public boolean hasAttributeChanged(String attribute) {
return this.attributeValidationResults.stream().filter(o -> o.getField().equals(attribute)).collect(Collectors.toList()).get(0).hasChanged();
}
}

View file

@ -0,0 +1,30 @@
/*
* 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;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public enum UserUpdateEvent {
UpdateProfile,
UserResource,
Account,
IdpReview,
RegistrationProfile,
RegistrationUserCreation
}

View file

@ -0,0 +1,44 @@
/*
* 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;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class ValidationResult {
boolean valid;
String errorType;
public ValidationResult( boolean valid, String errorType) {
this.errorType = errorType;
this.valid = valid;
}
public boolean isValid() {
return valid;
}
protected boolean isInvalid() {
return !isValid();
}
public String getErrorType() {
return errorType;
}
}

View file

@ -88,3 +88,4 @@ org.keycloak.headers.SecurityHeadersSpi
org.keycloak.services.clientpolicy.ClientPolicySpi org.keycloak.services.clientpolicy.ClientPolicySpi
org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi
org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi
org.keycloak.userprofile.UserProfileSpi

View file

@ -21,7 +21,6 @@ import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -32,13 +31,23 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.resources.AttributeFormDataProcessor; import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.represenations.AttributeUserProfile;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.IdpUserProfile;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -97,9 +106,17 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
event.event(EventType.UPDATE_PROFILE); event.event(EventType.UPDATE_PROFILE);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
RealmModel realm = context.getRealm(); UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = userCtx.getEmail();
String newEmail = updatedProfile.getFirstAttribute(UserModel.EMAIL);
DefaultUserProfileContext updateContext =
new DefaultUserProfileContext(UserUpdateEvent.IdpReview, new IdpUserProfile(userCtx), updatedProfile);
UserProfileValidationResult result = profileProvider.validate(updateContext);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData, userCtx.isEditUsernameAllowed());
if (errors != null && !errors.isEmpty()) { if (errors != null && !errors.isEmpty()) {
Response challenge = context.form() Response challenge = context.form()
.setErrors(errors) .setErrors(errors)
@ -110,28 +127,37 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
return; return;
} }
String username = realm.isRegistrationEmailAsUsername() ? formData.getFirst(UserModel.EMAIL) : formData.getFirst(UserModel.USERNAME); UserProfileUpdateHelper.update(UserUpdateEvent.IdpReview, context.getSession(), new UserModelDelegate(null) {
userCtx.setUsername(username); @Override
userCtx.setFirstName(formData.getFirst(UserModel.FIRST_NAME)); public Map<String, List<String>> getAttributes() {
userCtx.setLastName(formData.getFirst(UserModel.LAST_NAME)); return userCtx.getAttributes();
String email = formData.getFirst(UserModel.EMAIL);
if (!ObjectUtil.isEqualOrBothNull(email, userCtx.getEmail())) {
if (logger.isTraceEnabled()) {
logger.tracef("Email updated on updateProfile page to '%s' ", email);
} }
userCtx.setEmail(email); @Override
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true"); public List<String> getAttribute(String name) {
} return userCtx.getAttribute(name);
}
AttributeFormDataProcessor.process(formData, realm, userCtx); @Override
public void setAttribute(String name, List<String> values) {
userCtx.setAttribute(name, values);
}
@Override
public void removeAttribute(String name) {
userCtx.getAttributes().remove(name);
}
}, updatedProfile);
userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE); userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE);
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername()); logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
event.detail(Details.UPDATED_EMAIL, 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();
}
event.detail(Details.UPDATED_EMAIL, newEmail);
// Ensure page is always shown when user later returns to it - for example with form "back" button // Ensure page is always shown when user later returns to it - for example with form "back" button
context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true"); context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");

View file

@ -21,23 +21,30 @@ import org.keycloak.Config;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormContext; import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation; 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.represenations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.UserModelUserProfile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -58,51 +65,27 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
} }
@Override @Override
public void validate(ValidationContext context) { public void validate(org.keycloak.authentication.ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>(); UserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
UserProfileProvider userProfile = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
context.getEvent().detail(Details.REGISTER_METHOD, "form"); context.getEvent().detail(Details.REGISTER_METHOD, "form");
String eventError = Errors.INVALID_REGISTRATION; DefaultUserProfileContext updateContext = new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, updatedProfile);
if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME)))) { UserProfileValidationResult result = userProfile.validate(updateContext);
errors.add(new FormMessage(RegistrationPage.FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME)); List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
}
if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME)))) {
errors.add(new FormMessage(RegistrationPage.FIELD_LAST_NAME, Messages.MISSING_LAST_NAME));
}
String email = formData.getFirst(Validation.FIELD_EMAIL);
boolean emailValid = true;
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
emailValid = false;
} else if (!Validation.isEmailValid(email)) {
context.getEvent().detail(Details.EMAIL, email);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
emailValid = false;
}
if (emailValid && !context.getRealm().isDuplicateEmailsAllowed()) {
boolean duplicateEmail = false;
try {
if(context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
duplicateEmail = true;
}
} catch (ModelDuplicateException e) {
duplicateEmail = true;
}
if (duplicateEmail) {
eventError = Errors.EMAIL_IN_USE;
formData.remove(Validation.FIELD_EMAIL);
context.getEvent().detail(Details.EMAIL, email);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
}
}
if (errors.size() > 0) { if (errors.size() > 0) {
context.error(eventError); if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL))
context.getEvent().detail(Details.EMAIL, updatedProfile.getFirstAttribute(UserModel.EMAIL));
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS)) {
context.error(Errors.EMAIL_IN_USE);
formData.remove("email");
} else
context.error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors); context.validationError(formData, errors);
return; return;
@ -114,10 +97,11 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
@Override @Override
public void success(FormContext context) { public void success(FormContext context) {
UserModel user = context.getUser(); UserModel user = context.getUser();
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME)); DefaultUserProfileContext updateContext =
user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL)); new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, new UserModelUserProfile(user), updatedProfile);
UserProfileUpdateHelper.update(updateContext.getUpdateEvent(), context.getSession(), user, updatedProfile, false);
} }
@Override @Override

View file

@ -37,9 +37,17 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor; import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation; 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.represenations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.UserModelUserProfile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -63,52 +71,39 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
@Override @Override
public void validate(ValidationContext context) { public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
context.getEvent().detail(Details.REGISTER_METHOD, "form"); context.getEvent().detail(Details.REGISTER_METHOD, "form");
String email = formData.getFirst(Validation.FIELD_EMAIL); UserProfile newProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
String username = formData.getFirst(RegistrationPage.FIELD_USERNAME); String email = newProfile.getFirstAttribute(UserModel.EMAIL);
context.getEvent().detail(Details.USERNAME, username); String username = newProfile.getFirstAttribute(UserModel.USERNAME);
context.getEvent().detail(Details.EMAIL, email); context.getEvent().detail(Details.EMAIL, email);
context.getEvent().detail(Details.USERNAME, username);
String usernameField = RegistrationPage.FIELD_USERNAME; UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
context.getEvent().detail(Details.REGISTER_METHOD, "form");
DefaultUserProfileContext updateContext = new DefaultUserProfileContext(UserUpdateEvent.RegistrationUserCreation, newProfile);
UserProfileValidationResult result = profileProvider.validate(updateContext);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (context.getRealm().isRegistrationEmailAsUsername()) { if (context.getRealm().isRegistrationEmailAsUsername()) {
context.getEvent().detail(Details.USERNAME, email); context.getEvent().detail(Details.USERNAME, email);
}
if (Validation.isBlank(email)) { if (errors.size() > 0) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL)); if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS)) {
} else if (!Validation.isEmailValid(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
formData.remove(Validation.FIELD_EMAIL);
}
if (errors.size() > 0) {
context.error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
}
if (email != null && !context.getRealm().isDuplicateEmailsAllowed() && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
context.error(Errors.EMAIL_IN_USE); context.error(Errors.EMAIL_IN_USE);
formData.remove(Validation.FIELD_EMAIL); formData.remove(RegistrationPage.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS)); } else if (result.hasFailureOfErrorType(Messages.MISSING_EMAIL, Messages.MISSING_USERNAME, Messages.INVALID_EMAIL)) {
context.validationError(formData, errors); if (result.hasFailureOfErrorType(Messages.INVALID_EMAIL))
return; formData.remove(Validation.FIELD_EMAIL);
}
} else {
if (Validation.isBlank(username)) {
context.error(Errors.INVALID_REGISTRATION); context.error(Errors.INVALID_REGISTRATION);
errors.add(new FormMessage(RegistrationPage.FIELD_USERNAME, Messages.MISSING_USERNAME)); } else if (result.hasFailureOfErrorType(Messages.USERNAME_EXISTS)) {
context.validationError(formData, errors);
return;
}
if (context.getSession().users().getUserByUsername(username, context.getRealm()) != null) {
context.error(Errors.USERNAME_IN_USE); context.error(Errors.USERNAME_IN_USE);
errors.add(new FormMessage(usernameField, Messages.USERNAME_EXISTS));
formData.remove(Validation.FIELD_USERNAME); formData.remove(Validation.FIELD_USERNAME);
context.validationError(formData, errors);
return;
} }
context.validationError(formData, errors);
return;
} }
context.success(); context.success();
} }
@ -120,22 +115,26 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
@Override @Override
public void success(FormContext context) { public void success(FormContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
String email = formData.getFirst(Validation.FIELD_EMAIL);
String username = formData.getFirst(RegistrationPage.FIELD_USERNAME); String email = updatedProfile.getFirstAttribute(UserModel.EMAIL);
String username = updatedProfile.getFirstAttribute(UserModel.USERNAME);
if (context.getRealm().isRegistrationEmailAsUsername()) { if (context.getRealm().isRegistrationEmailAsUsername()) {
username = formData.getFirst(RegistrationPage.FIELD_EMAIL); username = email;
} }
context.getEvent().detail(Details.USERNAME, username) context.getEvent().detail(Details.USERNAME, username)
.detail(Details.REGISTER_METHOD, "form") .detail(Details.REGISTER_METHOD, "form")
.detail(Details.EMAIL, email) .detail(Details.EMAIL, email);
;
UserModel user = context.getSession().users().addUser(context.getRealm(), username); UserModel user = context.getSession().users().addUser(context.getRealm(), username);
user.setEnabled(true); user.setEnabled(true);
user.setEmail(email); DefaultUserProfileContext updateContext =
new DefaultUserProfileContext(UserUpdateEvent.RegistrationUserCreation, new UserModelUserProfile(user), updatedProfile);
UserProfileUpdateHelper.update(updateContext.getUpdateEvent(), context.getSession(), user, updatedProfile, false);
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
AttributeFormDataProcessor.process(formData, context.getRealm(), user);
context.setUser(user); context.setUser(user);
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().success(); context.getEvent().success();

View file

@ -19,18 +19,28 @@ package org.keycloak.authentication.requiredactions;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*; import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor; import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.represenations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.UserModelUserProfile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -63,11 +73,18 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
event.event(EventType.UPDATE_PROFILE); event.event(EventType.UPDATE_PROFILE);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
UserModel user = context.getUser(); UserModel user = context.getUser();
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = user.getEmail();
String newEmail = updatedProfile.getFirstAttribute(UserModel.EMAIL);
UserProfileProvider userProfile = context.getSession().getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
DefaultUserProfileContext updateContext =
new DefaultUserProfileContext(UserUpdateEvent.UpdateProfile, new UserModelUserProfile(user), updatedProfile);
UserProfileValidationResult result = userProfile.validate(updateContext);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
if (errors != null && !errors.isEmpty()) { if (errors != null && !errors.isEmpty()) {
Response challenge = context.form() Response challenge = context.form()
.setErrors(errors) .setErrors(errors)
@ -77,59 +94,10 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
return; return;
} }
if (realm.isEditUsernameAllowed()) { UserProfileUpdateHelper.update(UserUpdateEvent.UpdateProfile, context.getSession(), user, updatedProfile);
String username = formData.getFirst("username"); if (result.hasAttributeChanged(UserModel.EMAIL)) {
String oldUsername = user.getUsername();
boolean usernameChanged = oldUsername != null ? !oldUsername.equals(username) : username != null;
if (usernameChanged) {
if (session.users().getUserByUsername(username, realm) != null) {
Response challenge = context.form()
.setError(Messages.USERNAME_EXISTS)
.setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
return;
}
user.setUsername(username);
}
}
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) {
if (!realm.isDuplicateEmailsAllowed()) {
UserModel userByEmail = session.users().getUserByEmail(email, realm);
// check for duplicated email
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
Response challenge = context.form()
.setError(Messages.EMAIL_EXISTS)
.setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
return;
}
}
user.setEmail(email);
user.setEmailVerified(false); user.setEmailVerified(false);
} event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail).success();
AttributeFormDataProcessor.process(formData, realm, user);
if (emailChanged) {
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
} }
context.success(); context.success();

View file

@ -17,41 +17,31 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.userprofile.profile.represenations.AttributeUserProfile;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class AttributeFormDataProcessor { public class AttributeFormDataProcessor {
/**
* Looks for "user.attributes." keys in the form data and sets the appropriate UserModel.attribute from it.
*
* @param formData
* @param realm
* @param user
*/
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UserModel user) {
UpdateProfileContext userCtx = new UserUpdateProfileContext(realm, user);
process(formData, realm, userCtx);
}
public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UpdateProfileContext user) {
public static AttributeUserProfile process(MultivaluedMap<String, String> formData) {
Map<String, List<String>> attributes= new HashMap<>();
for (String key : formData.keySet()) { for (String key : formData.keySet()) {
if (!key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) continue; if (!key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) continue;
String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length()); String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
// Need to handle case when attribute has multiple values, but in UI was displayed just first value // Need to handle case when attribute has multiple values, but in UI was displayed just first value
List<String> modelVal = user.getAttribute(attribute); List<String> modelValue = new ArrayList<String>();
List<String> modelValue = modelVal==null ? new ArrayList<String>() : new ArrayList<>(modelVal);
int index = 0; int index = 0;
for (String value : formData.get(key)) { for (String value : formData.get(key)) {
@ -59,10 +49,29 @@ public class AttributeFormDataProcessor {
index++; index++;
} }
user.setAttribute(attribute, modelValue); attributes.put(attribute, modelValue);
} }
return new AttributeUserProfile(attributes);
} }
public static AttributeUserProfile toUserProfile(MultivaluedMap<String, String> formData) {
AttributeUserProfile profile = process(formData);
copyAttribute(UserModel.USERNAME, formData, profile);
copyAttribute(UserModel.FIRST_NAME, formData, profile);
copyAttribute(UserModel.LAST_NAME, formData, profile);
copyAttribute(UserModel.EMAIL, formData, profile);
return profile;
}
private static void copyAttribute(String key, MultivaluedMap<String, String> formData, AttributeUserProfile rep) {
if (formData.getFirst(key) != null)
rep.setSingleAttribute(key, formData.getFirst(key));
}
private static void addOrSetValue(List<String> list, int index, String value) { private static void addOrSetValue(List<String> list, int index, String value) {
if (list.size() > index) { if (list.size() > index) {
list.set(index, value); list.set(index, value);

View file

@ -28,9 +28,6 @@ import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.OTPCredentialProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.Event; import org.keycloak.events.Event;
@ -48,7 +45,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -77,6 +73,14 @@ import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.profile.represenations.AttributeUserProfile;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.UserModelUserProfile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.CredentialHelper; import org.keycloak.utils.CredentialHelper;
@ -358,37 +362,51 @@ public class AccountFormService extends AbstractSecuredLocalService {
csrfCheck(formData); csrfCheck(formData);
UserModel user = auth.getUser(); UserModel user = auth.getUser();
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(formData);
String oldEmail = user.getEmail();
String newEmail = updatedProfile.getFirstAttribute(UserModel.EMAIL);
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
if (errors != null && !errors.isEmpty()) { DefaultUserProfileContext updateContext =
new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(user), updatedProfile);
UserProfileValidationResult result = profileProvider.validate(updateContext);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
if (!errors.isEmpty()) {
setReferrerOnPage(); setReferrerOnPage();
return account.setErrors(Status.OK, errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); Response.Status status = Status.OK;
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME)) {
status = Response.Status.BAD_REQUEST;
} else if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.USERNAME_EXISTS)) {
status = Response.Status.CONFLICT;
}
return account.setErrors(status, errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
} }
try { try {
updateUsername(formData.getFirst("username"), user, session); UserProfileUpdateHelper.update(UserUpdateEvent.Account, session, user, updatedProfile);
updateEmail(formData.getFirst("email"), user, session, event); } catch (ReadOnlyException e) {
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
AttributeFormDataProcessor.process(formData, realm, user);
event.success();
setReferrerOnPage();
return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT);
} catch (ReadOnlyException roe) {
setReferrerOnPage(); setReferrerOnPage();
return account.setError(Response.Status.BAD_REQUEST, Messages.READ_ONLY_USER).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); return account.setError(Response.Status.BAD_REQUEST, Messages.READ_ONLY_USER).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
} catch (ModelDuplicateException mde) {
setReferrerOnPage();
return account.setError(Response.Status.CONFLICT, mde.getMessage()).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
} }
if (result.hasAttributeChanged(UserModel.EMAIL)) {
user.setEmailVerified(false);
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail).success();
}
event.success();
setReferrerOnPage();
return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT);
} }
@Path("sessions") @Path("sessions")
@POST @POST
public Response processSessionsLogout(final MultivaluedMap<String, String> formData) { public Response processSessionsLogout(final MultivaluedMap<String, String> formData) {
@ -1057,53 +1075,6 @@ public class AccountFormService extends AbstractSecuredLocalService {
} }
} }
private void updateUsername(String username, UserModel user, KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
boolean usernameChanged = username == null || !user.getUsername().equals(username);
if (realm.isEditUsernameAllowed() && !realm.isRegistrationEmailAsUsername()) {
if (usernameChanged) {
UserModel existing = session.users().getUserByUsername(username, realm);
if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.USERNAME_EXISTS);
}
user.setUsername(username);
}
} else if (usernameChanged) {
}
}
private void updateEmail(String email, UserModel user, KeycloakSession session, EventBuilder event) {
RealmModel realm = session.getContext().getRealm();
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(email, realm);
if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.EMAIL_EXISTS);
}
}
user.setEmail(email);
if (emailChanged) {
user.setEmailVerified(false);
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
}
if (realm.isRegistrationEmailAsUsername()) {
if (!realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(email, realm);
if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.USERNAME_EXISTS);
}
}
user.setUsername(email);
}
}
private void csrfCheck(final MultivaluedMap<String, String> formData) { private void csrfCheck(final MultivaluedMap<String, String> formData) {
String formStateChecker = formData.getFirst("stateChecker"); String formStateChecker = formData.getFirst("stateChecker");
if (formStateChecker == null || !formStateChecker.equals(this.stateChecker)) { if (formStateChecker == null || !formStateChecker.equals(this.stateChecker)) {

View file

@ -20,8 +20,9 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.enums.AccountRestApiVersion; import org.keycloak.common.enums.AccountRestApiVersion;
import org.keycloak.common.Profile;
import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.events.Details; import org.keycloak.credential.CredentialModel;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -46,6 +47,17 @@ import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.account.resources.ResourcesService; import org.keycloak.services.resources.account.resources.ResourcesService;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.theme.Theme;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.represenations.AccountUserRepresentationUserProfile;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.represenations.UserModelUserProfile;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import org.keycloak.common.Profile;
import org.keycloak.theme.Theme;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
@ -63,6 +75,7 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -73,9 +86,6 @@ import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.keycloak.common.Profile;
import org.keycloak.theme.Theme;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -163,83 +173,26 @@ public class AccountRestService {
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public Response updateAccount(UserRepresentation userRep) { public Response updateAccount(UserRepresentation rep) {
auth.require(AccountRoles.MANAGE_ACCOUNT); auth.require(AccountRoles.MANAGE_ACCOUNT);
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(user); event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
AccountUserRepresentationUserProfile updatedUser = new AccountUserRepresentationUserProfile(rep);
DefaultUserProfileContext updateContext =
new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(user), updatedUser);
UserProfileValidationResult result = profileProvider.validate(updateContext);
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME))
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
if (result.hasFailureOfErrorType(Messages.USERNAME_EXISTS))
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS))
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
try { try {
RealmModel realm = session.getContext().getRealm(); UserProfileUpdateHelper.update(UserUpdateEvent.Account, session, user, updatedUser);
boolean usernameChanged = userRep.getUsername() != null && !userRep.getUsername().equals(user.getUsername());
if (realm.isEditUsernameAllowed()) {
if (usernameChanged) {
UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm);
if (existing != null) {
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
}
user.setUsername(userRep.getUsername());
}
} else if (usernameChanged) {
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
}
boolean emailChanged = userRep.getEmail() != null && !userRep.getEmail().equals(user.getEmail());
if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm);
if (existing != null) {
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
}
}
if (emailChanged && realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm);
if (existing != null) {
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
}
}
if (emailChanged) {
String oldEmail = user.getEmail();
user.setEmail(userRep.getEmail());
user.setEmailVerified(false);
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, userRep.getEmail()).success();
if (realm.isRegistrationEmailAsUsername()) {
user.setUsername(userRep.getEmail());
}
}
user.setFirstName(userRep.getFirstName());
user.setLastName(userRep.getLastName());
if (userRep.getAttributes() != null) {
Set<String> attributeKeys = new HashSet<>(user.getAttributes().keySet());
// We store username and other attributes as attributes (for future UserProfile)
// but don't propagate them to the UserRepresentation, so userRep will never contain them
// if the user did not explicitly add them
attributeKeys.remove(UserModel.FIRST_NAME);
attributeKeys.remove(UserModel.LAST_NAME);
attributeKeys.remove(UserModel.EMAIL);
attributeKeys.remove(UserModel.USERNAME);
for (String k : attributeKeys) {
if (!userRep.getAttributes().containsKey(k)) {
user.removeAttribute(k);
}
}
Map<String, List<String>> attributes = userRep.getAttributes();
// Make sure we don't accidentally update any of the fields through attributes
attributes.remove(UserModel.FIRST_NAME);
attributes.remove(UserModel.LAST_NAME);
attributes.remove(UserModel.EMAIL);
attributes.remove(UserModel.USERNAME);
for (Map.Entry<String, List<String>> e : userRep.getAttributes().entrySet()) {
user.setAttribute(e.getKey(), e.getValue());
}
}
event.success(); event.success();
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build(); return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
@ -275,6 +228,39 @@ public class AccountRestService {
// TODO Federated identities // TODO Federated identities
/**
* Returns the applications with the given id in the specified realm.
*
* @param clientId client id to search for
* @return application with the provided id
*/
@Path("/applications/{clientId}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getApplication(final @PathParam("clientId") String clientId) {
checkAccountApiEnabled();
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS);
ClientModel client = realm.getClientByClientId(clientId);
if (client == null || client.isBearerOnly() || client.getBaseUrl() == null) {
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity("No client with clientId: " + clientId + " found.")).build();
}
List<String> inUseClients = new LinkedList<>();
if (!session.sessions().getUserSessions(realm, client).isEmpty()) {
inUseClients.add(clientId);
}
List<String> offlineClients = new LinkedList<>();
if (session.sessions().getOfflineSessionsCount(realm, client) > 0) {
offlineClients.add(clientId);
}
UserConsentModel consentModel = session.users().getConsentByClient(realm, user.getId(), client.getId());
Map<String, UserConsentModel> consentModels = Collections.singletonMap(client.getClientId(), consentModel);
return Cors.add(request, Response.ok(modelToRepresentation(client, inUseClients, offlineClients, consentModels))).build();
}
private ClientRepresentation modelToRepresentation(ClientModel model, List<String> inUseClients, List<String> offlineClients, Map<String, UserConsentModel> consents) { private ClientRepresentation modelToRepresentation(ClientModel model, List<String> inUseClients, List<String> offlineClients, Map<String, UserConsentModel> consents) {
ClientRepresentation representation = new ClientRepresentation(); ClientRepresentation representation = new ClientRepresentation();
representation.setClientId(model.getClientId()); representation.setClientId(model.getClientId());

View file

@ -18,8 +18,6 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken; import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
@ -73,12 +71,17 @@ import org.keycloak.services.resources.account.AccountFormService;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.userprofile.utils.UserProfileUpdateHelper;
import org.keycloak.userprofile.profile.represenations.UserRepresentationUserProfile;
import org.keycloak.userprofile.validation.UserUpdateEvent;
import org.keycloak.utils.ProfileHelper; import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
@ -156,17 +159,6 @@ public class UserResource {
auth.users().requireManage(user); auth.users().requireManage(user);
try { try {
Set<String> attrsToRemove;
if (rep.getAttributes() != null) {
attrsToRemove = new HashSet<>(user.getAttributes().keySet());
attrsToRemove.removeAll(rep.getAttributes().keySet());
attrsToRemove.remove(UserModel.FIRST_NAME);
attrsToRemove.remove(UserModel.LAST_NAME);
attrsToRemove.remove(UserModel.EMAIL);
attrsToRemove.remove(UserModel.USERNAME);
} else {
attrsToRemove = Collections.emptySet();
}
if (rep.isEnabled() != null && rep.isEnabled()) { if (rep.isEnabled() != null && rep.isEnabled()) {
UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, user.getId()); UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, user.getId());
@ -175,7 +167,7 @@ public class UserResource {
} }
} }
updateUserFromRep(user, rep, attrsToRemove, realm, session, true); updateUserFromRep(user, rep, session, true);
RepresentationToModel.createCredentials(rep, session, realm, user, true); RepresentationToModel.createCredentials(rep, session, realm, user, true);
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success(); adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success();
@ -198,20 +190,9 @@ public class UserResource {
} }
} }
public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove, RealmModel realm, KeycloakSession session, boolean removeMissingRequiredActions) { public static void updateUserFromRep(UserModel user, UserRepresentation rep, KeycloakSession session, boolean removeMissingRequiredActions) {
if (rep.getUsername() != null && realm.isEditUsernameAllowed() && !realm.isRegistrationEmailAsUsername()) {
user.setUsername(rep.getUsername()); UserProfileUpdateHelper.update(UserUpdateEvent.UserResource, session, user, new UserRepresentationUserProfile(rep));
}
if (rep.getEmail() != null) {
String email = rep.getEmail();
user.setEmail(email);
if(realm.isRegistrationEmailAsUsername()) {
user.setUsername(email);
}
}
if (rep.getEmail() == "") user.setEmail(null);
if (rep.getFirstName() != null) user.setFirstName(rep.getFirstName());
if (rep.getLastName() != null) user.setLastName(rep.getLastName());
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled()); if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified()); if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
@ -243,21 +224,9 @@ public class UserResource {
} }
} }
} }
if (rep.getAttributes() != null) {
for (Map.Entry<String, List<String>> attr : rep.getAttributes().entrySet()) {
List<String> currentValue = user.getAttribute(attr.getKey());
if (currentValue == null || currentValue.size() != attr.getValue().size() || !currentValue.containsAll(attr.getValue())) {
user.setAttribute(attr.getKey(), attr.getValue());
}
}
for (String attr : attrsToRemove) {
user.removeAttribute(attr);
}
}
} }
/** /**
* Get representation of the user * Get representation of the user
* *
@ -323,10 +292,10 @@ public class UserResource {
result.put("sameRealm", sameRealm); result.put("sameRealm", sameRealm);
result.put("redirect", redirect.toString()); result.put("redirect", redirect.toString());
event.event(EventType.IMPERSONATE) event.event(EventType.IMPERSONATE)
.session(userSession) .session(userSession)
.user(user) .user(user)
.detail(Details.IMPERSONATOR_REALM, authenticatedRealm.getName()) .detail(Details.IMPERSONATOR_REALM, authenticatedRealm.getName())
.detail(Details.IMPERSONATOR, impersonator).success(); .detail(Details.IMPERSONATOR, impersonator).success();
return result; return result;
} }

View file

@ -18,7 +18,6 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import javax.ws.rs.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.ObjectUtil;
@ -36,16 +35,15 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.policy.PasswordPolicyNotMetException; import org.keycloak.policy.PasswordPolicyNotMetException;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@ -55,15 +53,12 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Base resource for managing users * Base resource for managing users
@ -156,9 +151,8 @@ public class UsersResource {
try { try {
UserModel user = session.users().addUser(realm, username); UserModel user = session.users().addUser(realm, username);
Set<String> emptySet = Collections.emptySet();
UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false); UserResource.updateUserFromRep(user, rep, session, false);
RepresentationToModel.createFederatedIdentities(rep, session, realm, user); RepresentationToModel.createFederatedIdentities(rep, session, realm, user);
RepresentationToModel.createGroups(rep, realm, user); RepresentationToModel.createGroups(rep, realm, user);

View file

@ -26,11 +26,14 @@ import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError; import org.keycloak.policy.PolicyError;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.userprofile.validation.AttributeValidationResult;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Validation { public class Validation {
@ -150,4 +153,12 @@ public class Validation {
} }
public static List<FormMessage> getFormErrorsFromValidation(UserProfileValidationResult results) {
List<FormMessage> errors = new ArrayList<>();
for (AttributeValidationResult result : results.getErrors()) {
result.getFailedValidations().forEach(o -> addError(errors, result.getField(), o.getErrorType()));
}
return errors;
}
} }

View file

@ -0,0 +1,119 @@
/*
* 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;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages;
import org.keycloak.userprofile.validation.StaticValidators;
import org.keycloak.userprofile.validation.UserProfileValidationResult;
import org.keycloak.userprofile.validation.ValidationChainBuilder;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class LegacyUserProfileProvider implements UserProfileProvider {
private static final Logger logger = Logger.getLogger(LegacyUserProfileProvider.class);
private KeycloakSession session;
public LegacyUserProfileProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void close() {
}
@Override
public UserProfileValidationResult validate(UserProfileContext updateContext) {
RealmModel realm = this.session.getContext().getRealm();
ValidationChainBuilder builder = ValidationChainBuilder.builder();
switch (updateContext.getUpdateEvent()) {
case UserResource:
break;
case IdpReview:
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername());
break;
case Account:
case RegistrationProfile:
case UpdateProfile:
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername() && realm.isEditUsernameAllowed());
addSessionValidators(builder);
break;
case RegistrationUserCreation:
addUserCreationValidators(builder);
break;
}
return new UserProfileValidationResult(builder.build().validate(updateContext));
}
private void addUserCreationValidators(ValidationChainBuilder builder) {
RealmModel realm = this.session.getContext().getRealm();
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()
.build();
} else {
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
.addValidationFunction(Messages.MISSING_USERNAME, StaticValidators.isBlank())
.addValidationFunction(Messages.USERNAME_EXISTS,
(value, o) -> session.users().getUserByUsername(value, realm) == null)
.build();
}
}
private void addBasicValidators(ValidationChainBuilder builder, boolean userNameExistsCondition) {
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
.addValidationFunction(Messages.MISSING_USERNAME, StaticValidators.checkUsernameExists(userNameExistsCondition)).build()
.addAttributeValidator().forAttribute(UserModel.FIRST_NAME)
.addValidationFunction(Messages.MISSING_FIRST_NAME, StaticValidators.isBlank()).build()
.addAttributeValidator().forAttribute(UserModel.LAST_NAME)
.addValidationFunction(Messages.MISSING_LAST_NAME, StaticValidators.isBlank()).build()
.addAttributeValidator().forAttribute(UserModel.EMAIL)
.addValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
.addValidationFunction(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()
.addAttributeValidator().forAttribute(UserModel.EMAIL)
.addValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.isEmailDuplicated(session))
.addValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.doesEmailExistAsUsername(session)).build()
.build();
}
}

View file

@ -0,0 +1,59 @@
/*
* 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;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class LegacyUserProfileProviderFactory implements UserProfileProviderFactory {
UserProfileProvider provider;
@Override
public UserProfileProvider create(KeycloakSession session) {
provider = new LegacyUserProfileProvider(session);
return provider;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
public static final String PROVIDER_ID = "legacy-user-profile";
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.utils.StoredUserProfile;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public abstract class AbstractUserProfile implements UserProfile , StoredUserProfile {
/*
The user attributes handling is different in each user representation so we have to use the setAttributes and getAttributes from the original object
*/
@Override
public abstract Map<String, List<String>> getAttributes();
@Override
public abstract void setAttribute(String key, List<String> value);
/*
The user id is different in each user representation
*/
@Override
public abstract String getId();
@Override
public void setSingleAttribute(String key, String value) {
this.setAttribute(key, Collections.singletonList(value));
}
@Override
public String getFirstAttribute(String key) {
return this.getAttributes() == null ? null : this.getAttributes().get(key) == null ? null : this.getAttributes().get(key).size() == 0 ? null : this.getAttributes().get(key).get(0);
}
@Override
public List<String> getAttribute(String key) {
return getAttributes().get(key);
}
@Override
public void removeAttribute(String attr) {
getAttributes().remove(attr);
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.utils.StoredUserProfile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.validation.UserUpdateEvent;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class DefaultUserProfileContext implements UserProfileContext {
private boolean isCreated;
private StoredUserProfile currentUserProfile;
private UserProfile updatedUserProfile;
private UserUpdateEvent userUpdateEvent;
public DefaultUserProfileContext(UserUpdateEvent userUpdateEvent, UserProfile updatedUserProfile) {
this.userUpdateEvent = userUpdateEvent;
this.isCreated = false;
this.currentUserProfile = null;
this.updatedUserProfile = updatedUserProfile;
}
public DefaultUserProfileContext(UserUpdateEvent userUpdateEvent, StoredUserProfile currentUserProfile, UserProfile updatedUserProfile) {
this.userUpdateEvent = userUpdateEvent;
this.isCreated = true;
this.currentUserProfile = currentUserProfile;
this.updatedUserProfile = updatedUserProfile;
}
@Override
public boolean isCreate() {
return isCreated;
}
@Override
public UserProfile getCurrent() {
return currentUserProfile;
}
@Override
public UserProfile getUpdated() {
return updatedUserProfile;
}
@Override
public UserUpdateEvent getUpdateEvent(){
return userUpdateEvent;
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.represenations;
import org.keycloak.models.UserModel;
import org.keycloak.representations.account.UserRepresentation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class AccountUserRepresentationUserProfile extends AttributeUserProfile {
public AccountUserRepresentationUserProfile(UserRepresentation user) {
super(flattenUserRepresentation(user));
}
private static Map<String, List<String>> flattenUserRepresentation(UserRepresentation user) {
Map<String, List<String>> attrs = new HashMap<>();
if (user.getAttributes() != null) attrs.putAll(user.getAttributes());
if (user.getUsername() != null)
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
else
attrs.remove(UserModel.USERNAME);
if (user.getEmail() != null)
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
else
attrs.remove(UserModel.EMAIL);
if (user.getLastName() != null)
attrs.put(UserModel.LAST_NAME, Collections.singletonList(user.getLastName()));
if (user.getFirstName() != null)
attrs.put(UserModel.FIRST_NAME, Collections.singletonList(user.getFirstName()));
return attrs;
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.represenations;
import org.keycloak.userprofile.profile.AbstractUserProfile;
import javax.ws.rs.NotSupportedException;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class AttributeUserProfile extends AbstractUserProfile {
private final Map<String, List<String>> attributes;
public AttributeUserProfile(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
@Override
public Map<String, List<String>> getAttributes() {
return this.attributes;
}
@Override
public void setAttribute(String key, List<String> value) {
this.getAttributes().put(key, value);
}
@Override
public String getId() {
throw new NotSupportedException("No ID support");
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.represenations;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.userprofile.profile.AbstractUserProfile;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class IdpUserProfile extends AbstractUserProfile {
private final SerializedBrokeredIdentityContext user;
public IdpUserProfile(SerializedBrokeredIdentityContext user) {
this.user = user;
}
@Override
public String getId() {
return user.getId();
}
@Override
public Map<String, List<String>> getAttributes() {
return user.getAttributes();
}
@Override
public void setAttribute(String key, List<String> value) {
user.setAttribute(key, value);
}
}

View file

@ -0,0 +1,57 @@
/*
* 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.represenations;
import org.keycloak.models.UserModel;
import org.keycloak.userprofile.profile.AbstractUserProfile;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class UserModelUserProfile extends AbstractUserProfile {
private final UserModel user;
public UserModelUserProfile(UserModel user) {
this.user = user;
}
@Override
public String getId() {
return user.getId();
}
@Override
public Map<String, List<String>> getAttributes() {
return user.getAttributes();
}
@Override
public void setAttribute(String key, List<String> value) {
user.setAttribute(key, value);
}
@Override
public void removeAttribute(String attr) {
// Due to the fact that the user attribute list is a copy and not a reference in the user adapter we have to access the remove function directly
user.removeAttribute(attr);
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.represenations;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class UserRepresentationUserProfile extends AttributeUserProfile {
public UserRepresentationUserProfile(UserRepresentation user) {
super(flattenUserRepresentation(user));
}
private static Map<String, List<String>> flattenUserRepresentation(UserRepresentation user) {
Map<String, List<String>> attrs = new HashMap<>();
if (user.getAttributes() != null) attrs.putAll(user.getAttributes());
if (user.getUsername() != null)
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
else
attrs.remove(UserModel.USERNAME);
if (user.getEmail() != null)
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
else
attrs.remove(UserModel.EMAIL);
if (user.getUsername() != null)
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
if (user.getLastName() != null)
attrs.put(UserModel.LAST_NAME, Collections.singletonList(user.getLastName()));
if (user.getFirstName() != null)
attrs.put(UserModel.FIRST_NAME, Collections.singletonList(user.getFirstName()));
if (user.getEmail() != null)
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
return attrs;
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.utils;
import org.keycloak.userprofile.UserProfile;
import java.util.List;
/**
* Abstraction, which allows to update the user in various contexts (Required action of already existing user, or first identity provider
* login when user doesn't yet exists in Keycloak DB)
*
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface StoredUserProfile extends UserProfile {
void setSingleAttribute(String name, String value);
void setAttribute(String key, List<String> value);
void removeAttribute(String attr);
}

View file

@ -0,0 +1,97 @@
/*
* 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.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.userprofile.validation.UserUpdateEvent;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class UserProfileUpdateHelper {
public static void update(UserUpdateEvent userUpdateEvent, KeycloakSession session, UserModel currentUser, StoredUserProfile updatedUser) {
update(userUpdateEvent, session, currentUser, updatedUser, true);
}
public static void update(UserUpdateEvent userUpdateEvent, KeycloakSession session, UserModel currentUser, StoredUserProfile updatedUser, boolean removeMissingAttributes) {
RealmModel realm = session.getContext().getRealm();
if (updatedUser.getAttributes() == null || updatedUser.getAttributes().size() == 0)
return;
//The Idp review does not respect "isEditUserNameAllowed" therefore we have to miss the check here
if (!userUpdateEvent.equals(UserUpdateEvent.IdpReview)) {
//This step has to be done before email is assigned to the username if isRegistrationEmailAsUsername is set
//Otherwise email change will not reflect in username changes.
if (updatedUser.getFirstAttribute(UserModel.USERNAME) != null && !realm.isEditUsernameAllowed()) {
updatedUser.removeAttribute(UserModel.USERNAME);
}
}
if (updatedUser.getFirstAttribute(UserModel.EMAIL) != null && updatedUser.getFirstAttribute(UserModel.EMAIL).isEmpty()) {
updatedUser.removeAttribute(UserModel.EMAIL);
// updatedUser.setAttribute(UserModel.EMAIL, Collections.singletonList(null));
}
if (updatedUser.getFirstAttribute(UserModel.EMAIL) != null && realm.isRegistrationEmailAsUsername()) {
updatedUser.removeAttribute(UserModel.USERNAME);
updatedUser.setAttribute(UserModel.USERNAME, Collections.singletonList(updatedUser.getFirstAttribute(UserModel.EMAIL)));
}
updateAttributes(currentUser, updatedUser.getAttributes(), removeMissingAttributes);
}
private static void updateAttributes(UserModel currentUser, Map<String, List<String>> updatedUser, boolean removeMissingAttributes) {
for (Map.Entry<String, List<String>> attr : updatedUser.entrySet()) {
List<String> currentValue = currentUser.getAttribute(attr.getKey());
//In case of username we need to provide lower case values
List<String> updatedValue = attr.getKey().equals(UserModel.USERNAME) ? AttributeToLower(attr.getValue()) : attr.getValue();
if ((currentValue == null || currentValue.size() != updatedValue.size() || !currentValue.containsAll(updatedValue))) {
currentUser.setAttribute(attr.getKey(), updatedValue);
}
}
if (removeMissingAttributes) {
Set<String> attrsToRemove = new HashSet<>(currentUser.getAttributes().keySet());
attrsToRemove.removeAll(updatedUser.keySet());
for (String attr : attrsToRemove) {
currentUser.removeAttribute(attr);
}
}
}
private static List<String> AttributeToLower(List<String> attr) {
if (attr.size() == 1 && attr.get(0) != null)
return Collections.singletonList(KeycloakModelUtils.toLowerCaseSafe(attr.get(0)));
return attr;
}
}

View file

@ -0,0 +1,34 @@
/*
* 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 java.util.List;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class AttributeValidator {
String attributeKey;
List<Validator> validators;
public AttributeValidator(String attributeKey, List<Validator> validators) {
this.validators = validators;
this.attributeKey = attributeKey;
}
}

View file

@ -0,0 +1,53 @@
/*
* 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 org.keycloak.userprofile.UserProfileContext;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class AttributeValidatorBuilder {
ValidationChainBuilder validationChainBuilder;
String attributeKey;
List<Validator> validations = new ArrayList<>();
public AttributeValidatorBuilder(ValidationChainBuilder validationChainBuilder) {
this.validationChainBuilder = validationChainBuilder;
}
public AttributeValidatorBuilder addValidationFunction(String messageKey, BiFunction<String, UserProfileContext, Boolean> validationFunction) {
this.validations.add(new Validator(messageKey, validationFunction));
return this;
}
public AttributeValidatorBuilder forAttribute(String attributeKey) {
this.attributeKey = attributeKey;
return this;
}
public ValidationChainBuilder build() {
this.validationChainBuilder.addValidatorConfig(new AttributeValidator(attributeKey, this.validations));
return this.validationChainBuilder;
}
}

View file

@ -0,0 +1,93 @@
/*
* 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 org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.UserProfileContext;
import java.util.function.BiFunction;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class StaticValidators {
public static BiFunction<String, UserProfileContext, Boolean> isBlank() {
return (value, context) ->
!Validation.isBlank(value);
}
public static BiFunction<String, UserProfileContext, Boolean> isEmailValid() {
return (value, context) ->
Validation.isBlank(value) || Validation.isEmailValid(value);
}
public static BiFunction<String, UserProfileContext, Boolean> userNameExists(KeycloakSession session) {
return (value, context) ->
!(context.getCurrent() != null
&& !value.equals(context.getCurrent().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()
&& context.getCurrent() != null
&& !value.equals(context.getCurrent().getFirstAttribute(UserModel.USERNAME))
);
}
public static BiFunction<String, UserProfileContext, Boolean> checkUsernameExists(boolean externalCondition) {
return (value, context) ->
!(externalCondition && Validation.isBlank(value));
}
public static BiFunction<String, UserProfileContext, Boolean> doesEmailExistAsUsername(KeycloakSession session) {
return (value, context) -> {
RealmModel realm = session.getContext().getRealm();
if (!realm.isDuplicateEmailsAllowed()) {
UserModel userByEmail = session.users().getUserByEmail(value, realm);
return !(realm.isRegistrationEmailAsUsername() && userByEmail != null && context.getCurrent() != null && !userByEmail.getId().equals(context.getCurrent().getId()));
}
return true;
};
}
public static BiFunction<String, UserProfileContext, Boolean> isEmailDuplicated(KeycloakSession session) {
return (value, context) -> {
RealmModel realm = session.getContext().getRealm();
if (!realm.isDuplicateEmailsAllowed()) {
UserModel userByEmail = session.users().getUserByEmail(value, realm);
// check for duplicated email
return !(userByEmail != null && (context.getCurrent() == null || !userByEmail.getId().equals(context.getCurrent().getId())));
}
return true;
};
}
public static BiFunction<String, UserProfileContext, Boolean> doesEmailExist(KeycloakSession session) {
return (value, context) ->
!(value != null
&& !session.getContext().getRealm().isDuplicateEmailsAllowed()
&& session.users().getUserByEmail(value, session.getContext().getRealm()) != null);
}
}

View file

@ -0,0 +1,56 @@
/*
* 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 org.keycloak.userprofile.UserProfileContext;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class ValidationChain {
List<AttributeValidator> attributeValidators;
public ValidationChain(List<AttributeValidator> attributeValidators) {
this.attributeValidators = attributeValidators;
}
public List<AttributeValidationResult> validate(UserProfileContext updateContext) {
List<AttributeValidationResult> overallResults = new ArrayList<>();
for (AttributeValidator attribute : attributeValidators) {
List<ValidationResult> validationResults = new ArrayList<>();
String attributeKey = attribute.attributeKey;
String attributeValue = updateContext.getUpdated().getFirstAttribute(attributeKey);
boolean attributeChanged = false;
if (attributeValue != null) {
attributeChanged = updateContext.getCurrent() != null && !attributeValue.equals(updateContext.getCurrent().getFirstAttribute(attributeKey));
for (Validator validator : attribute.validators) {
validationResults.add(new ValidationResult(validator.function.apply(attributeValue, updateContext), validator.errorType));
}
}
overallResults.add(new AttributeValidationResult(attributeKey, attributeChanged, validationResults));
}
return overallResults;
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class ValidationChainBuilder {
Map<String, AttributeValidator> attributeConfigs = new HashMap<>();
public static ValidationChainBuilder builder() {
return new ValidationChainBuilder();
}
public AttributeValidatorBuilder addAttributeValidator() {
return new AttributeValidatorBuilder(this);
}
public ValidationChain build() {
return new ValidationChain(this.attributeConfigs.values().stream().collect(Collectors.toList()));
}
public void addValidatorConfig(AttributeValidator validator) {
if (attributeConfigs.containsKey(validator.attributeKey)) {
attributeConfigs.get(validator.attributeKey).validators.addAll(validator.validators);
} else {
attributeConfigs.put(validator.attributeKey, validator);
}
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 org.keycloak.userprofile.UserProfileContext;
import java.util.function.BiFunction;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class Validator {
String errorType;
BiFunction<String, UserProfileContext, Boolean> function;
public Validator(String errorType, BiFunction<String, UserProfileContext, Boolean> function) {
this.function = function;
this.errorType = errorType;
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.userprofile.LegacyUserProfileProviderFactory

View file

@ -0,0 +1,74 @@
package org.keycloak.userprofile.validation;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.profile.represenations.UserRepresentationUserProfile;
import java.util.Collections;
import java.util.stream.Collectors;
public class ValidationChainTest {
ValidationChainBuilder builder;
ValidationChain testchain;
UserProfile user;
DefaultUserProfileContext updateContext;
@Before
public void setUp() throws Exception {
builder = ValidationChainBuilder.builder()
.addAttributeValidator().forAttribute("FAKE_FIELD")
.addValidationFunction("FAKE_FIELD_ERRORKEY", (value, updateUserProfileContext) -> !value.equals("content")).build()
.addAttributeValidator().forAttribute("firstName")
.addValidationFunction("FIRST_NAME_FIELD_ERRORKEY", (value, updateUserProfileContext) -> true).build();
UserRepresentation rep = new UserRepresentation();
//default user content
rep.singleAttribute(UserModel.FIRST_NAME, "firstName");
rep.singleAttribute(UserModel.LAST_NAME, "lastName");
rep.singleAttribute(UserModel.EMAIL, "email");
rep.singleAttribute("FAKE_FIELD", "content");
rep.singleAttribute("NULLABLE_FIELD", null);
user = new UserRepresentationUserProfile(rep);
updateContext = new DefaultUserProfileContext(UserUpdateEvent.Account,null, user);
}
@Test
public void validate() {
testchain = builder.build();
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext));
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());
Assert.assertEquals(2, results.getValidationResults().size());
}
@Test
public void mergedConfig() {
testchain = builder.addAttributeValidator().forAttribute("FAKE_FIELD")
.addValidationFunction("FAKE_FIELD_ERRORKEY_1", (value, updateUserProfileContext) -> false).build()
.addAttributeValidator().forAttribute("FAKE_FIELD")
.addValidationFunction("FAKE_FIELD_ERRORKEY_2", (value, updateUserProfileContext) -> false).build().build();
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext));
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"));
}
@Test
public void emptyChain() {
UserProfileValidationResult results = new UserProfileValidationResult(ValidationChainBuilder.builder().build().validate(updateContext));
Assert.assertEquals(Collections.emptyList(), results.getValidationResults());
}
}

View file

@ -61,6 +61,7 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.TokenUtil; import org.keycloak.testsuite.util.TokenUtil;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
@ -78,7 +79,6 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.ACCOUNT_API; import static org.keycloak.common.Profile.Feature.ACCOUNT_API;
import org.keycloak.testsuite.util.UserBuilder;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -99,6 +99,49 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
assertTrue(user.getAttributes().isEmpty()); assertTrue(user.getAttributes().isEmpty());
} }
@Test
public void testUpdateSingleField() throws IOException {
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
String originalUsername = user.getUsername();
String originalFirstName = user.getFirstName();
String originalLastName = user.getLastName();
String originalEmail = user.getEmail();
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
realmRep.setRegistrationEmailAsUsername(false);
adminClient.realm("test").update(realmRep);
user.setFirstName(null);
user.setLastName("Bob");
user.setEmail(null);
user.getAttributes().clear();
user = updateAndGet(user);
assertEquals(user.getLastName(), "Bob");
assertEquals(user.getFirstName(), originalFirstName);
assertEquals(user.getEmail(), originalEmail);
} finally {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
realmRep.setEditUsernameAllowed(true);
adminClient.realm("test").update(realmRep);
user.setUsername(originalUsername);
user.setFirstName(originalFirstName);
user.setLastName(originalLastName);
user.setEmail(originalEmail);
user.setAttributes(originalAttributes);
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
System.out.println(response.asString());
assertEquals(204, response.getStatus());
}
}
@Test @Test
public void testUpdateProfile() throws IOException { public void testUpdateProfile() throws IOException {
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
@ -109,6 +152,11 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes()); Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
try { try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
realmRep.setRegistrationEmailAsUsername(false);
adminClient.realm("test").update(realmRep);
user.setFirstName("Homer"); user.setFirstName("Homer");
user.setLastName("Simpsons"); user.setLastName("Simpsons");
user.getAttributes().put("attr1", Collections.singletonList("val1")); user.getAttributes().put("attr1", Collections.singletonList("val1"));
@ -146,11 +194,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user = updateAndGet(user); user = updateAndGet(user);
assertEquals("test-user@localhost", user.getEmail()); assertEquals("test-user@localhost", user.getEmail());
// Update username
user.setUsername("updatedUsername");
user = updateAndGet(user);
assertEquals("updatedusername", user.getUsername());
user.setUsername("john-doh@localhost"); user.setUsername("john-doh@localhost");
updateError(user, 409, Messages.USERNAME_EXISTS); updateError(user, 409, Messages.USERNAME_EXISTS);
@ -158,8 +201,24 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user = updateAndGet(user); user = updateAndGet(user);
assertEquals("test-user@localhost", user.getUsername()); assertEquals("test-user@localhost", user.getUsername());
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
realmRep.setRegistrationEmailAsUsername(true);
adminClient.realm("test").update(realmRep);
user.setUsername("updatedUsername");
user = updateAndGet(user);
assertEquals("test-user@localhost", user.getUsername());
realmRep.setRegistrationEmailAsUsername(false);
adminClient.realm("test").update(realmRep);
user.setUsername("updatedUsername");
user = updateAndGet(user);
assertEquals("updatedusername", user.getUsername());
realmRep.setEditUsernameAllowed(false); realmRep.setEditUsernameAllowed(false);
realmRep.setRegistrationEmailAsUsername(false);
adminClient.realm("test").update(realmRep); adminClient.realm("test").update(realmRep);
user.setUsername("updatedUsername2"); user.setUsername("updatedUsername2");

View file

@ -275,6 +275,7 @@ public class LDAPProvidersIntegrationNoImportTest extends LDAPProvidersIntegrati
UserRepresentation johnRep = john.toRepresentation(); UserRepresentation johnRep = john.toRepresentation();
String firstNameOrig = johnRep.getFirstName(); String firstNameOrig = johnRep.getFirstName();
String lastNameOrig = johnRep.getLastName(); String lastNameOrig = johnRep.getLastName();
String emailOrig = johnRep.getEmail();
String postalCodeOrig = johnRep.getAttributes().get("postal_code").get(0); String postalCodeOrig = johnRep.getAttributes().get("postal_code").get(0);
try { try {
@ -327,6 +328,10 @@ public class LDAPProvidersIntegrationNoImportTest extends LDAPProvidersIntegrati
johnRep.setLastName(lastNameOrig); johnRep.setLastName(lastNameOrig);
johnRep.singleAttribute("postal_code", postalCodeOrig); johnRep.singleAttribute("postal_code", postalCodeOrig);
john.update(johnRep); john.update(johnRep);
Assert.assertEquals(firstNameOrig, johnRep.getFirstName());
Assert.assertEquals(lastNameOrig, johnRep.getLastName());
Assert.assertEquals(emailOrig, johnRep.getEmail());
Assert.assertEquals(postalCodeOrig, johnRep.getAttributes().get("postal_code").get(0));
} }
} }