Allow managing the required settigs for the email attribute
Closes #15026
This commit is contained in:
parent
782d145cef
commit
857b02be63
15 changed files with 190 additions and 48 deletions
|
@ -20,7 +20,6 @@
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -47,7 +46,7 @@ public final class AttributeMetadata {
|
||||||
private final Predicate<AttributeContext> selector;
|
private final Predicate<AttributeContext> selector;
|
||||||
private final List<Predicate<AttributeContext>> writeAllowed = new ArrayList<>();
|
private final List<Predicate<AttributeContext>> writeAllowed = new ArrayList<>();
|
||||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||||
private final Predicate<AttributeContext> required;
|
private Predicate<AttributeContext> required;
|
||||||
private final List<Predicate<AttributeContext>> readAllowed = new ArrayList<>();
|
private final List<Predicate<AttributeContext>> readAllowed = new ArrayList<>();
|
||||||
private List<AttributeValidatorMetadata> validators;
|
private List<AttributeValidatorMetadata> validators;
|
||||||
private Map<String, Object> annotations;
|
private Map<String, Object> annotations;
|
||||||
|
@ -170,7 +169,7 @@ public final class AttributeMetadata {
|
||||||
return validators;
|
return validators;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addValidator(List<AttributeValidatorMetadata> validators) {
|
public AttributeMetadata addValidators(List<AttributeValidatorMetadata> validators) {
|
||||||
if (this.validators == null) {
|
if (this.validators == null) {
|
||||||
this.validators = new ArrayList<>();
|
this.validators = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
@ -202,7 +201,7 @@ public final class AttributeMetadata {
|
||||||
// we clone validators list to allow adding or removing validators. Validators
|
// we clone validators list to allow adding or removing validators. Validators
|
||||||
// itself are not cloned as we do not expect them to be reconfigured.
|
// itself are not cloned as we do not expect them to be reconfigured.
|
||||||
if (validators != null) {
|
if (validators != null) {
|
||||||
cloned.addValidator(validators);
|
cloned.addValidators(validators);
|
||||||
}
|
}
|
||||||
//we clone annotations map to allow adding to or removing from it
|
//we clone annotations map to allow adding to or removing from it
|
||||||
if(annotations != null) {
|
if(annotations != null) {
|
||||||
|
@ -247,4 +246,14 @@ public final class AttributeMetadata {
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return attributeName.hashCode();
|
return attributeName.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata setRequired(Predicate<AttributeContext> required) {
|
||||||
|
this.required = required;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata setValidators(List<AttributeValidatorMetadata> validators) {
|
||||||
|
this.validators = validators;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,19 +61,23 @@ public final class UserProfileMetadata implements Cloneable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
|
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
|
||||||
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidator(Arrays.asList(validator)));
|
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidators(Arrays.asList(validator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
|
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
|
||||||
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidator(validators));
|
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidators(validators));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, List<AttributeValidatorMetadata> validators) {
|
||||||
|
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, required, ALWAYS_TRUE).addValidators(validators));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validators) {
|
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validators) {
|
||||||
return addAttribute(new AttributeMetadata(name, guiOrder).addValidator(validators));
|
return addAttribute(new AttributeMetadata(name, guiOrder).addValidators(validators));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
|
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
|
||||||
return addAttribute(new AttributeMetadata(name, guiOrder, selector, writeAllowed, required, readAllowed).addValidator(validator));
|
return addAttribute(new AttributeMetadata(name, guiOrder, selector, writeAllowed, required, readAllowed).addValidators(validator));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -298,7 +298,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createRegistrationUserCreationProfile() {
|
private UserProfileMetadata createRegistrationUserCreationProfile() {
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
|
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID));
|
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID), new AttributeValidatorMetadata(UsernameHasValueValidator.ID));
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID));
|
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID));
|
||||||
|
|
||||||
|
@ -366,6 +366,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createUserResourceValidation(Config.Scope config) {
|
private UserProfileMetadata createUserResourceValidation(Config.Scope config) {
|
||||||
Pattern p = getRegexPatternString(config.getArray("admin-read-only-attributes"));
|
Pattern p = getRegexPatternString(config.getArray("admin-read-only-attributes"));
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
|
UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
|
||||||
|
|
||||||
|
|
||||||
|
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(UsernameHasValueValidator.ID));
|
||||||
|
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()));
|
||||||
|
|
||||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||||
|
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
|
|
|
@ -61,7 +61,6 @@ import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
||||||
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||||
import org.keycloak.validate.AbstractSimpleValidator;
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
import org.keycloak.validate.ValidatorConfig;
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
import org.keycloak.validate.validators.EmailValidator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link UserProfileProvider} loading configuration from the changeable JSON file stored in component config. Parsed
|
* {@link UserProfileProvider} loading configuration from the changeable JSON file stored in component config. Parsed
|
||||||
|
@ -295,7 +294,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE;
|
Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE;
|
||||||
if (rc != null && !isUsernameOrEmailAttribute(attributeName)) {
|
if (rc != null) {
|
||||||
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
||||||
required = AttributeMetadata.ALWAYS_TRUE;
|
required = AttributeMetadata.ALWAYS_TRUE;
|
||||||
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
|
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
|
||||||
|
@ -327,7 +326,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
Predicate<AttributeContext> selector = AttributeMetadata.ALWAYS_TRUE;
|
Predicate<AttributeContext> selector = AttributeMetadata.ALWAYS_TRUE;
|
||||||
UPAttributeSelector sc = attrConfig.getSelector();
|
UPAttributeSelector sc = attrConfig.getSelector();
|
||||||
if (sc != null && !isUsernameOrEmailAttribute(attributeName) && UPConfigUtils.canBeAuthFlowContext(context) && sc.getScopes() != null && !sc.getScopes().isEmpty()) {
|
if (sc != null && !isBuiltInAttribute(attributeName) && UPConfigUtils.canBeAuthFlowContext(context) && sc.getScopes() != null && !sc.getScopes().isEmpty()) {
|
||||||
// for contexts executed from auth flow and with configured scopes selector
|
// for contexts executed from auth flow and with configured scopes selector
|
||||||
// we have to create correct predicate
|
// we have to create correct predicate
|
||||||
selector = (c) -> requestedScopePredicate(c, sc.getScopes());
|
selector = (c) -> requestedScopePredicate(c, sc.getScopes());
|
||||||
|
@ -337,48 +336,46 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
String attributeGroup = attrConfig.getGroup();
|
String attributeGroup = attrConfig.getGroup();
|
||||||
AttributeGroupMetadata groupMetadata = toAttributeGroupMeta(groupsByName.get(attributeGroup));
|
AttributeGroupMetadata groupMetadata = toAttributeGroupMeta(groupsByName.get(attributeGroup));
|
||||||
|
|
||||||
if (isUsernameOrEmailAttribute(attributeName)) {
|
guiOrder++;
|
||||||
|
|
||||||
|
if (isBuiltInAttribute(attributeName)) {
|
||||||
// make sure username and email are writable if permissions are not set
|
// make sure username and email are writable if permissions are not set
|
||||||
if (permissions == null || permissions.isEmpty()) {
|
if (permissions == null || permissions.isEmpty()) {
|
||||||
writeAllowed = AttributeMetadata.ALWAYS_TRUE;
|
writeAllowed = AttributeMetadata.ALWAYS_TRUE;
|
||||||
readAllowed = AttributeMetadata.ALWAYS_TRUE;
|
readAllowed = AttributeMetadata.ALWAYS_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
if (UserModel.USERNAME.equals(attributeName)) {
|
||||||
|
required = AttributeMetadata.ALWAYS_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// Add ImmutableAttributeValidator to ensure that attributes that are configured
|
// Add ImmutableAttributeValidator to ensure that attributes that are configured
|
||||||
// as read-only are marked as such.
|
// as read-only are marked as such.
|
||||||
// Skip this for username in realms with username = email to allow change of email
|
// Skip this for username in realms with username = email to allow change of email
|
||||||
// address on initial login with profile via idp
|
// address on initial login with profile via idp
|
||||||
if (!realm.isRegistrationEmailAsUsername() || !UserModel.USERNAME.equals(attributeName)) {
|
if (!realm.isRegistrationEmailAsUsername() && UserModel.EMAIL.equals(attributeName)) {
|
||||||
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atts.isEmpty()) {
|
List<AttributeMetadata> existingMetadata = decoratedMetadata.getAttribute(attributeName);
|
||||||
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
|
||||||
// doesn't require it.
|
|
||||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, writeAllowed, validators)
|
|
||||||
.addAnnotations(annotations)
|
|
||||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
|
||||||
.setAttributeGroupMetadata(groupMetadata);
|
|
||||||
} else {
|
|
||||||
final int localGuiOrder = guiOrder++;
|
|
||||||
Predicate<AttributeContext> readAllowedFinal = readAllowed;
|
|
||||||
Predicate<AttributeContext> writeAllowedFinal = writeAllowed;
|
|
||||||
|
|
||||||
// add configured validators and annotations to existing attribute metadata
|
if (existingMetadata.isEmpty()) {
|
||||||
atts.stream().forEach(c -> c.addValidator(validators)
|
throw new IllegalStateException("Attribute " + attributeName + " not defined in the context.");
|
||||||
.addAnnotations(annotations)
|
}
|
||||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
|
||||||
.setGuiOrder(localGuiOrder)
|
for (AttributeMetadata metadata : existingMetadata) {
|
||||||
.setAttributeGroupMetadata(groupMetadata)
|
metadata.addAnnotations(annotations)
|
||||||
.addReadCondition(readAllowedFinal)
|
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||||
.addWriteCondition(writeAllowedFinal));
|
.setGuiOrder(guiOrder)
|
||||||
|
.setAttributeGroupMetadata(groupMetadata)
|
||||||
|
.addReadCondition(readAllowed)
|
||||||
|
.addWriteCondition(writeAllowed)
|
||||||
|
.addValidators(validators)
|
||||||
|
.setRequired(required);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// always add validation for immutable/read-only attributes
|
|
||||||
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
||||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, validators, selector, writeAllowed, required, readAllowed)
|
decoratedMetadata.addAttribute(attributeName, guiOrder, validators, selector, writeAllowed, required, readAllowed)
|
||||||
.addAnnotations(annotations)
|
.addAnnotations(annotations)
|
||||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||||
.setAttributeGroupMetadata(groupMetadata);
|
.setAttributeGroupMetadata(groupMetadata);
|
||||||
|
@ -400,7 +397,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
return new AttributeGroupMetadata(group.getName(), group.getDisplayHeader(), group.getDisplayDescription(), group.getAnnotations());
|
return new AttributeGroupMetadata(group.getName(), group.getDisplayHeader(), group.getDisplayDescription(), group.getAnnotations());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isUsernameOrEmailAttribute(String attributeName) {
|
private boolean isBuiltInAttribute(String attributeName) {
|
||||||
return UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName);
|
return UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.userprofile.config;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration of the User Profile for one realm.
|
* Configuration of the User Profile for one realm.
|
||||||
|
@ -70,6 +71,16 @@ public class UPConfig {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public UPAttribute getAttribute(String name) {
|
||||||
|
for (UPAttribute attribute : getAttributes()) {
|
||||||
|
if (attribute.getName().equals(name)) {
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "UPConfig [attributes=" + attributes + ", groups=" + groups + "]";
|
return "UPConfig [attributes=" + attributes + ", groups=" + groups + "]";
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.userprofile.validator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
import org.keycloak.validate.SimpleValidator;
|
import org.keycloak.validate.SimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -56,6 +58,12 @@ public class BlankAttributeValidator implements SimpleValidator {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AttributeContext attributeContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
||||||
|
|
||||||
|
if (!attributeContext.getMetadata().isRequired(attributeContext)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
String value = values.isEmpty() ? null: values.get(0);
|
String value = values.isEmpty() ? null: values.get(0);
|
||||||
|
|
||||||
if ((failOnNull || value != null) && Validation.isBlank(value)) {
|
if ((failOnNull || value != null) && Validation.isBlank(value)) {
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
{
|
{
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"displayName": "${username}",
|
"displayName": "${username}",
|
||||||
|
"permissions": {
|
||||||
|
"view": ["admin", "user"],
|
||||||
|
"edit": ["admin", "user"]
|
||||||
|
},
|
||||||
"validations": {
|
"validations": {
|
||||||
"length": { "min": 3, "max": 255 },
|
"length": { "min": 3, "max": 255 },
|
||||||
"username-prohibited-characters": {}
|
"username-prohibited-characters": {}
|
||||||
|
@ -11,6 +15,11 @@
|
||||||
{
|
{
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"displayName": "${email}",
|
"displayName": "${email}",
|
||||||
|
"required": {"roles" : ["user"]},
|
||||||
|
"permissions": {
|
||||||
|
"view": ["admin", "user"],
|
||||||
|
"edit": ["admin", "user"]
|
||||||
|
},
|
||||||
"validations": {
|
"validations": {
|
||||||
"email" : {},
|
"email" : {},
|
||||||
"length": { "max": 255 }
|
"length": { "max": 255 }
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.actions;
|
package org.keycloak.testsuite.actions;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -258,7 +262,10 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
|
||||||
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
||||||
Assert.assertEquals("", updateProfilePage.getEmail());
|
Assert.assertEquals("", updateProfilePage.getEmail());
|
||||||
|
|
||||||
Assert.assertEquals("Please specify email.", updateProfilePage.getInputErrors().getEmailError());
|
assertThat(updateProfilePage.getInputErrors().getEmailError(), anyOf(
|
||||||
|
containsString("Please specify email"),
|
||||||
|
containsString("Please specify this field")
|
||||||
|
));
|
||||||
|
|
||||||
events.assertEmpty();
|
events.assertEmpty();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.actions;
|
package org.keycloak.testsuite.actions;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -226,7 +229,10 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
|
||||||
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
||||||
Assert.assertEquals("", updateProfilePage.getEmail());
|
Assert.assertEquals("", updateProfilePage.getEmail());
|
||||||
|
|
||||||
Assert.assertEquals("Please specify email.", updateProfilePage.getInputErrors().getEmailError());
|
assertThat(updateProfilePage.getInputErrors().getEmailError(), anyOf(
|
||||||
|
containsString("Please specify email"),
|
||||||
|
containsString("Please specify this field")
|
||||||
|
));
|
||||||
|
|
||||||
events.assertEmpty();
|
events.assertEmpty();
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,7 +227,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
|
||||||
user.setUsername(PARENT3_USERNAME);
|
user.setUsername(PARENT3_USERNAME);
|
||||||
user.setFirstName("first name");
|
user.setFirstName("first name");
|
||||||
user.setLastName("last name");
|
user.setLastName("last name");
|
||||||
user.setEmail("email");
|
user.setEmail("email@keycloak.org");
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
||||||
}
|
}
|
||||||
|
@ -749,7 +749,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
|
||||||
Assert.assertEquals(PARENT3_USERNAME, token.getPreferredUsername());
|
Assert.assertEquals(PARENT3_USERNAME, token.getPreferredUsername());
|
||||||
Assert.assertEquals("first name", token.getGivenName());
|
Assert.assertEquals("first name", token.getGivenName());
|
||||||
Assert.assertEquals("last name", token.getFamilyName());
|
Assert.assertEquals("last name", token.getFamilyName());
|
||||||
Assert.assertEquals("email", token.getEmail());
|
Assert.assertEquals("email@keycloak.org", token.getEmail());
|
||||||
|
|
||||||
// cleanup remove the user
|
// cleanup remove the user
|
||||||
childRealm.users().get(token.getSubject()).remove();
|
childRealm.users().get(token.getSubject()).remove();
|
||||||
|
|
|
@ -52,6 +52,7 @@ import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
import javax.mail.internet.MimeMessage;
|
import javax.mail.internet.MimeMessage;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
@ -515,7 +516,9 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertTrue(registerPage.isCurrent());
|
assertTrue(registerPage.isCurrent());
|
||||||
assertEquals("Invalid password: must not be equal to the username.", registerPage.getInputPasswordErrors().getPasswordError());
|
assertEquals("Invalid password: must not be equal to the username.", registerPage.getInputPasswordErrors().getPasswordError());
|
||||||
|
|
||||||
adminClient.realm("test").users().create(UserBuilder.create().username("registerUserNotUsername").build());
|
try (Response response = adminClient.realm("test").users().create(UserBuilder.create().username("registerUserNotUsername").build())) {
|
||||||
|
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
registerPage.register("firstName", "lastName", "registerUserNotUsername@email", "registerUserNotUsername", "registerUserNotUsername", "registerUserNotUsername");
|
registerPage.register("firstName", "lastName", "registerUserNotUsername@email", "registerUserNotUsername", "registerUserNotUsername", "registerUserNotUsername");
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
|
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
|
||||||
|
@ -59,7 +61,7 @@ public class RegisterWithUserProfileTest extends RegisterTest {
|
||||||
private static ClientRepresentation client_scope_optional;
|
private static ClientRepresentation client_scope_optional;
|
||||||
|
|
||||||
public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\": {}},";
|
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\": {\"roles\" : [\"user\"]}},";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
@ -602,6 +604,64 @@ public class RegisterWithUserProfileTest extends RegisterTest {
|
||||||
assertEquals(null, user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
assertEquals(null, user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailAsOptional() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "lastName", null, "registerWithoutEmail", "password", "password");
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailRequired() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\": {}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "lastName", null, "registerWithoutEmail", "password", "password");
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
assertThat(registerPage.getInputAccountErrors().getEmailError(), anyOf(
|
||||||
|
containsString("Please specify email"),
|
||||||
|
containsString("Please specify this field")
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailRequiredForUser() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\": {\"roles\" : [\"user\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "lastName", null, "registerWithoutEmail", "password", "password");
|
||||||
|
assertThat(registerPage.getInputAccountErrors().getEmailError(), anyOf(
|
||||||
|
containsString("Please specify email"),
|
||||||
|
containsString("Please specify this field")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private void assertUserRegistered(String userId, String username, String email, String firstName, String lastName) {
|
private void assertUserRegistered(String userId, String username, String email, String firstName, String lastName) {
|
||||||
events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
|
events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
|
||||||
|
|
|
@ -184,7 +184,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void failValidationWhenEmptyAttributes(KeycloakSession session) {
|
private static void failValidationWhenEmptyAttributes(KeycloakSession session) throws IOException {
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
provider.setConfiguration(null);
|
provider.setConfiguration(null);
|
||||||
|
@ -227,6 +227,14 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
realm.setRegistrationEmailAsUsername(false);
|
realm.setRegistrationEmailAsUsername(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UPConfig config = JsonSerialization.readValue(provider.getConfiguration(), UPConfig.class);
|
||||||
|
|
||||||
|
UPAttribute email = config.getAttribute("email");
|
||||||
|
|
||||||
|
email.setRequired(null);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
attributes.clear();
|
attributes.clear();
|
||||||
attributes.put(UserModel.USERNAME, "profile-user");
|
attributes.put(UserModel.USERNAME, "profile-user");
|
||||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
@ -438,6 +446,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, userName);
|
attributes.put(UserModel.USERNAME, userName);
|
||||||
|
attributes.put(UserModel.EMAIL, "user@keycloak.org");
|
||||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
attributes.put("address", "fixed-address");
|
attributes.put("address", "fixed-address");
|
||||||
|
@ -457,6 +466,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
Map<String, String> attributesUpdatedOldValues = new HashMap<>();
|
Map<String, String> attributesUpdatedOldValues = new HashMap<>();
|
||||||
attributesUpdatedOldValues.put(UserModel.FIRST_NAME, "Joe");
|
attributesUpdatedOldValues.put(UserModel.FIRST_NAME, "Joe");
|
||||||
attributesUpdatedOldValues.put(UserModel.LAST_NAME, "Doe");
|
attributesUpdatedOldValues.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
attributesUpdatedOldValues.put(UserModel.EMAIL, "user@keycloak.org");
|
||||||
|
|
||||||
profile.update((attributeName, userModel, oldValue) -> {
|
profile.update((attributeName, userModel, oldValue) -> {
|
||||||
assertTrue(attributesUpdated.add(attributeName));
|
assertTrue(attributesUpdated.add(attributeName));
|
||||||
|
@ -856,6 +866,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
provider.setConfiguration(null);
|
provider.setConfiguration(null);
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
attributes.put(UserModel.EMAIL, "user@keycloak.org");
|
||||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
|
||||||
|
|
|
@ -1634,7 +1634,19 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
|
||||||
return attributeName != "username" && attributeName != "email";
|
return attributeName != "username" && attributeName != "email";
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.guiOrderUp = function(index) {
|
$scope.showRequiredSettings = function(attributeName) {
|
||||||
|
if (attributeName == "username") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeName == "email" && realm.registrationEmailAsUsername) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.guiOrderUp = function(index) {
|
||||||
$scope.moveAttribute(index, index - 1);
|
$scope.moveAttribute(index, index - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
<input type="hidden" ui-select2="selectorByScopeSelect" id="selectorByScopeSelect" data-ng-model="selectorByScope" data-placeholder="Select a scope..." multiple/>
|
<input type="hidden" ui-select2="selectorByScopeSelect" id="selectorByScopeSelect" data-ng-model="selectorByScope" data-placeholder="Select a scope..." multiple/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="isNotUsernameOrEmail(currentAttribute.name)">
|
<div class="form-group" data-ng-show="showRequiredSettings(currentAttribute.name)">
|
||||||
<label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label>
|
<label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label>
|
||||||
<kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -160,14 +160,14 @@
|
||||||
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="isRequired">
|
<div class="form-group" data-ng-show="isRequired && showRequiredSettings(currentAttribute.name)">
|
||||||
<label class="col-md-2 control-label" for="isRequiredRoles">{{:: 'user.profile.attribute.required.roles' | translate}}</label>
|
<label class="col-md-2 control-label" for="isRequiredRoles">{{:: 'user.profile.attribute.required.roles' | translate}}</label>
|
||||||
<kc-tooltip>{{:: 'user.profile.attribute.required.roles.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'user.profile.attribute.required.roles.tooltip' | translate}}</kc-tooltip>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input type="hidden" ui-select2="isRequiredRoles" id="isRequiredRoles" data-ng-model="requiredRoles" data-placeholder="Select a role..." multiple/>
|
<input type="hidden" ui-select2="isRequiredRoles" id="isRequiredRoles" data-ng-model="requiredRoles" data-placeholder="Select a role..." multiple/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="isRequired">
|
<div class="form-group" data-ng-show="isRequired && showRequiredSettings(currentAttribute.name)">
|
||||||
<label class="col-md-2 control-label" for="isRequiredScopes">{{:: 'user.profile.attribute.required.scopes' | translate}}</label>
|
<label class="col-md-2 control-label" for="isRequiredScopes">{{:: 'user.profile.attribute.required.scopes' | translate}}</label>
|
||||||
<kc-tooltip>{{:: 'user.profile.attribute.required.scopes.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'user.profile.attribute.required.scopes.tooltip' | translate}}</kc-tooltip>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
Loading…
Reference in a new issue