Allow managing the required settigs for the email attribute

Closes #15026
This commit is contained in:
Pedro Igor 2022-10-20 02:04:25 -03:00
parent 782d145cef
commit 857b02be63
15 changed files with 190 additions and 48 deletions

View file

@ -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;
}
} }

View file

@ -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));
} }
/** /**

View file

@ -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) {

View file

@ -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) }
for (AttributeMetadata metadata : existingMetadata) {
metadata.addAnnotations(annotations)
.setAttributeDisplayName(attrConfig.getDisplayName()) .setAttributeDisplayName(attrConfig.getDisplayName())
.setGuiOrder(localGuiOrder) .setGuiOrder(guiOrder)
.setAttributeGroupMetadata(groupMetadata) .setAttributeGroupMetadata(groupMetadata)
.addReadCondition(readAllowedFinal) .addReadCondition(readAllowed)
.addWriteCondition(writeAllowedFinal)); .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);
} }

View file

@ -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 + "]";

View file

@ -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)) {

View file

@ -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 }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();

View file

@ -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");

View file

@ -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();

View file

@ -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");

View file

@ -1634,6 +1634,18 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
return attributeName != "username" && attributeName != "email"; return attributeName != "username" && attributeName != "email";
}; };
$scope.showRequiredSettings = function(attributeName) {
if (attributeName == "username") {
return false;
}
if (attributeName == "email" && realm.registrationEmailAsUsername) {
return false;
}
return true;
};
$scope.guiOrderUp = function(index) { $scope.guiOrderUp = function(index) {
$scope.moveAttribute(index, index - 1); $scope.moveAttribute(index, index - 1);
}; };

View file

@ -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">