UserProfile M1
This commit is contained in:
parent
efa16b5ac4
commit
72f73f153a
44 changed files with 1916 additions and 391 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue