mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 06:20:05 -06:00
Do not show update email link if the email attribute is not writable
Closes #39669 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
@@ -119,11 +119,15 @@ export const PersonalInfo = () => {
|
||||
((key: unknown, params) =>
|
||||
t(key as TFuncKey, params as any)) as TFunction
|
||||
}
|
||||
renderer={(attribute) =>
|
||||
attribute.name === "email" &&
|
||||
updateEmailFeatureEnabled &&
|
||||
updateEmailActionEnabled &&
|
||||
(!isRegistrationEmailAsUsername || isEditUserNameAllowed) ? (
|
||||
renderer={(attribute) => {
|
||||
const annotations = attribute.annotations
|
||||
? attribute.annotations
|
||||
: {};
|
||||
return attribute.name === "email" &&
|
||||
updateEmailFeatureEnabled &&
|
||||
updateEmailActionEnabled &&
|
||||
annotations["kc.required.action.supported"] &&
|
||||
(!isRegistrationEmailAsUsername || isEditUserNameAllowed) ? (
|
||||
<Button
|
||||
id="update-email-btn"
|
||||
variant="link"
|
||||
@@ -135,8 +139,8 @@ export const PersonalInfo = () => {
|
||||
>
|
||||
{t("updateEmail")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
) : undefined;
|
||||
}}
|
||||
/>
|
||||
{!allFieldsReadOnly() && (
|
||||
<ActionGroup>
|
||||
|
||||
@@ -567,6 +567,19 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||
return unmanagedAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAnnotations(String name) {
|
||||
AttributeMetadata metadata = getMetadata(name);
|
||||
|
||||
if (metadata == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
AttributeContext context = createAttributeContext(metadata);
|
||||
|
||||
return metadata.getAnnotations(context);
|
||||
}
|
||||
|
||||
protected AttributeMetadata createUnmanagedAttributeMetadata(String name) {
|
||||
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
||||
final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||
|
||||
@@ -155,12 +155,14 @@ public class UserProfileUtil {
|
||||
group = am.getAttributeGroupMetadata().getName();
|
||||
}
|
||||
|
||||
Attributes attributes = profile.getAttributes();
|
||||
|
||||
return new UserProfileAttributeMetadata(am.getName(),
|
||||
am.getAttributeDisplayName(),
|
||||
profile.getAttributes().isRequired(am.getName()),
|
||||
profile.getAttributes().isReadOnly(am.getName()),
|
||||
attributes.isRequired(am.getName()),
|
||||
attributes.isReadOnly(am.getName()),
|
||||
group,
|
||||
am.getAnnotations(),
|
||||
attributes.getAnnotations(am.getName()),
|
||||
toValidatorMetadata(am, session),
|
||||
am.isMultivalued());
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -52,7 +53,7 @@ public class AttributeMetadata {
|
||||
private Map<String, Object> annotations;
|
||||
private int guiOrder;
|
||||
private boolean multivalued;
|
||||
|
||||
private Function<AttributeContext, Map<String, Object>> annotationDecorator = (c) -> c.getMetadata().getAnnotations();
|
||||
|
||||
AttributeMetadata(String attributeName, int guiOrder) {
|
||||
this(attributeName, guiOrder, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||
@@ -225,6 +226,7 @@ public class AttributeMetadata {
|
||||
cloned.setAttributeGroupMetadata(attributeGroupMetadata.clone());
|
||||
}
|
||||
cloned.setMultivalued(multivalued);
|
||||
cloned.setAnnotationDecorator(annotationDecorator);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@@ -270,4 +272,13 @@ public class AttributeMetadata {
|
||||
this.validators = validators;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations(AttributeContext context) {
|
||||
return annotationDecorator.apply(context);
|
||||
}
|
||||
|
||||
public AttributeMetadata setAnnotationDecorator(Function<AttributeContext, Map<String, Object>> annotationDecorator) {
|
||||
this.annotationDecorator = annotationDecorator;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
@@ -168,4 +169,24 @@ public interface Attributes {
|
||||
* @return a map with any unmanaged attribute
|
||||
*/
|
||||
Map<String, List<String>> getUnmanagedAttributes();
|
||||
|
||||
/**
|
||||
* <p>Returns the annotations for an attribute with the given {@code name}.
|
||||
*
|
||||
* <p>The annotations returned by this method might differ from those returned directly from
|
||||
* the {@link AttributeMetadata#getAnnotations()} if the implementation supports annotations
|
||||
* being resolved dynamically based on contextual data. See {@link AttributeMetadata#setAnnotationDecorator(Function)}.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
* @return the annotations
|
||||
*/
|
||||
default Map<String, Object> getAnnotations(String name) {
|
||||
AttributeMetadata metadata = getMetadata(name);
|
||||
|
||||
if (metadata == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return metadata.getAnnotations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -42,13 +43,14 @@ import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.validator.OrganizationMemberValidator;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
@@ -57,7 +59,6 @@ import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueVali
|
||||
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
|
||||
import org.keycloak.userprofile.validator.DuplicateUsernameValidator;
|
||||
import org.keycloak.userprofile.validator.EmailExistsAsUsernameValidator;
|
||||
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||
import org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator;
|
||||
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator;
|
||||
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValidator;
|
||||
@@ -67,6 +68,7 @@ import org.keycloak.userprofile.validator.UsernameMutationValidator;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
||||
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
|
||||
@@ -412,7 +414,8 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
|
||||
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID),
|
||||
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
|
||||
.setAttributeDisplayName("${email}");
|
||||
.setAttributeDisplayName("${email}")
|
||||
.setAnnotationDecorator(DeclarativeUserProfileProviderFactory::getEmailAnnotationDecorator);
|
||||
|
||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||
|
||||
@@ -485,7 +488,7 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||
// The user-defined configuration is always parsed during init and should be avoided as much as possible
|
||||
// If no user-defined configuration is set, the system default configuration must have been set
|
||||
// In Quarkus, the system default configuration is set at build time for optimization purposes
|
||||
UPConfig defaultConfig = Optional.ofNullable(config.get("configFile"))
|
||||
UPConfig defaultConfig = ofNullable(config.get("configFile"))
|
||||
.map(Paths::get)
|
||||
.map(UPConfigUtils::parseConfig)
|
||||
.orElse(PARSED_DEFAULT_RAW_CONFIG);
|
||||
@@ -498,4 +501,36 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||
PARSED_DEFAULT_RAW_CONFIG = null;
|
||||
setDefaultConfig(defaultConfig);
|
||||
}
|
||||
|
||||
private static Map<String, Object> getEmailAnnotationDecorator(AttributeContext c) {
|
||||
AttributeMetadata m = c.getMetadata();
|
||||
Map<String, Object> rawAnnotations = Optional.ofNullable(m.getAnnotations()).orElse(Map.of());
|
||||
|
||||
if (Profile.isFeatureEnabled(Feature.UPDATE_EMAIL)) {
|
||||
UserProfileProvider provider = c.getSession().getProvider(UserProfileProvider.class);
|
||||
UPConfig upConfig = provider.getConfiguration();
|
||||
UPAttribute attribute = upConfig.getAttribute(UserModel.EMAIL);
|
||||
UPAttributePermissions permissions = attribute.getPermissions();
|
||||
|
||||
if (permissions == null) {
|
||||
return rawAnnotations;
|
||||
}
|
||||
|
||||
Set<String> writePermissions = permissions.getEdit();
|
||||
boolean isWritable = writePermissions.contains(UPConfigUtils.ROLE_USER);
|
||||
RealmModel realm = c.getSession().getContext().getRealm();
|
||||
|
||||
if ((realm.isRegistrationEmailAsUsername() && !realm.isEditUsernameAllowed()) || !isWritable) {
|
||||
return rawAnnotations;
|
||||
}
|
||||
|
||||
Map<String, Object> annotations = new HashMap<>(rawAnnotations);
|
||||
|
||||
annotations.put("kc.required.action.supported", isWritable);
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
return rawAnnotations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
*/
|
||||
package org.keycloak.testsuite.account;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -25,6 +28,8 @@ import static org.keycloak.testsuite.account.AccountRestServiceTest.getUserProfi
|
||||
import static org.keycloak.testsuite.util.userprofile.UserProfileUtil.PERMISSIONS_ALL;
|
||||
import static org.keycloak.testsuite.util.userprofile.UserProfileUtil.PERMISSIONS_ADMIN_EDITABLE;
|
||||
import static org.keycloak.testsuite.util.userprofile.UserProfileUtil.PERMISSIONS_ADMIN_ONLY;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
@@ -32,10 +37,13 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.UserModel;
|
||||
@@ -43,6 +51,10 @@ import org.keycloak.representations.idm.UserProfileAttributeMetadata;
|
||||
import org.keycloak.representations.idm.UserProfileMetadata;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
|
||||
import org.keycloak.testsuite.util.userprofile.UserProfileUtil;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
@@ -158,6 +170,33 @@ public class AccountRestServiceWithUserProfileTest extends AbstractRestServiceTe
|
||||
}
|
||||
}
|
||||
|
||||
@EnableFeature(value = Profile.Feature.UPDATE_EMAIL, skipRestart = true)
|
||||
@Test
|
||||
public void testUpdateEmailLink() throws Exception {
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation realmRep = realm.toRepresentation();
|
||||
|
||||
try {
|
||||
realmRep.setEditUsernameAllowed(false);
|
||||
realm.update(realmRep);
|
||||
|
||||
UserRepresentation user = getUser();
|
||||
assertNotNull(user.getUserProfileMetadata());
|
||||
assertThat(user.getUserProfileMetadata().getAttributeMetadata(UserModel.EMAIL).getAnnotations().get("kc.required.action.supported"), is(true));
|
||||
|
||||
UPConfig upConfig = realm.users().userProfile().getConfiguration();
|
||||
UPAttribute attribute = upConfig.getAttribute(UserModel.EMAIL);
|
||||
attribute.setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN)));
|
||||
realm.users().userProfile().update(upConfig);
|
||||
user = getUser();
|
||||
assertNotNull(user.getUserProfileMetadata());
|
||||
assertThat(user.getUserProfileMetadata().getAttributeMetadata(UserModel.EMAIL).getAnnotations().get("kc.required.action.supported"), is(nullValue()));
|
||||
} finally {
|
||||
realmRep.setEditUsernameAllowed(true);
|
||||
realm.update(realmRep);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUserProfileMetadata_RoAccessToNameFields() throws IOException {
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
|
||||
package org.keycloak.testsuite.user.profile;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -2311,6 +2313,77 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableFeature(Feature.UPDATE_EMAIL)
|
||||
@Test
|
||||
public void testEmailAnnotationsInAccountContext() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testEmailAnnotationsInAccountContext);
|
||||
}
|
||||
|
||||
private static void testEmailAnnotationsInAccountContext(KeycloakSession session) {
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, userName);
|
||||
String originalEmail = userName + "@keycloak.org";
|
||||
attributes.put(UserModel.EMAIL, originalEmail);
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put("address", "some address");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
UserModel user = profile.create();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
try {
|
||||
realm.setEditUsernameAllowed(false);
|
||||
realm.setRegistrationEmailAsUsername(true);
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
assertFalse(ofNullable(profile.getAttributes().getAnnotations(UserModel.EMAIL)).orElse(Map.of()).containsKey("kc.required.action.supported"));
|
||||
} finally {
|
||||
realm.setEditUsernameAllowed(true);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
}
|
||||
|
||||
try {
|
||||
realm.setEditUsernameAllowed(true);
|
||||
realm.setRegistrationEmailAsUsername(true);
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
assertThat(ofNullable(profile.getAttributes().getAnnotations(UserModel.EMAIL)).orElse(Map.of()).get("kc.required.action.supported"), is(true));
|
||||
} finally {
|
||||
realm.setEditUsernameAllowed(true);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
}
|
||||
|
||||
try {
|
||||
realm.setEditUsernameAllowed(false);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
UPConfig upConfig = provider.getConfiguration();
|
||||
UPAttribute attribute = upConfig.getAttribute(UserModel.EMAIL);
|
||||
attribute.setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN)));
|
||||
provider.setConfiguration(upConfig);
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
assertFalse(ofNullable(profile.getAttributes().getAnnotations(UserModel.EMAIL)).orElse(Map.of()).containsKey("kc.required.action.supported"));
|
||||
} finally {
|
||||
realm.setEditUsernameAllowed(true);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
}
|
||||
|
||||
try {
|
||||
realm.setEditUsernameAllowed(false);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
UPConfig upConfig = provider.getConfiguration();
|
||||
UPAttribute attribute = upConfig.getAttribute(UserModel.EMAIL);
|
||||
attribute.setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN, ROLE_USER)));
|
||||
provider.setConfiguration(upConfig);
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
assertThat(ofNullable(profile.getAttributes().getAnnotations(UserModel.EMAIL)).orElse(Map.of()).get("kc.required.action.supported"), is(true));
|
||||
} finally {
|
||||
realm.setEditUsernameAllowed(true);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultivalued() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testMultivalued);
|
||||
|
||||
Reference in New Issue
Block a user