Support for the locale user attribute

Closes #21163
This commit is contained in:
Pedro Igor 2023-06-22 19:42:02 -03:00
parent db9b6c2152
commit d0691b0884
7 changed files with 169 additions and 12 deletions

View file

@ -142,7 +142,16 @@ public interface Attributes {
if (includeBuiltin) { if (includeBuiltin) {
return true; return true;
} }
return !isRootAttribute(entry.getKey()); if (isRootAttribute(entry.getKey())) {
if (UserModel.LOCALE.equals(entry.getKey()) && !entry.getValue().isEmpty()) {
// locale is different form of built-in attribute in the sense it is related to a
// specific feature (i18n) and does not have a top-level attribute in the user representation
// the locale should be available from the attribute map if not empty
return true;
}
return false;
}
return true;
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
@ -163,7 +172,8 @@ public interface Attributes {
return UserModel.USERNAME.equals(name) return UserModel.USERNAME.equals(name)
|| UserModel.EMAIL.equals(name) || UserModel.EMAIL.equals(name)
|| UserModel.FIRST_NAME.equals(name) || UserModel.FIRST_NAME.equals(name)
|| UserModel.LAST_NAME.equals(name); || UserModel.LAST_NAME.equals(name)
|| UserModel.LOCALE.equals(name);
} }
Map<String, List<String>> toMap(); Map<String, List<String>> toMap();

View file

@ -145,6 +145,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
return null; return null;
} }
private static boolean isInternationalizationEnabled(AttributeContext context) {
RealmModel realm = context.getSession().getContext().getRealm();
return realm.isInternationalizationEnabled();
}
/** /**
* There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where * There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
* user profiles are used. They are related to internal attributes with hard conditions on them in terms of management. * user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
@ -199,7 +204,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
} }
addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createAccountProfile(ACCOUNT, readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT_OLD, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT_OLD, readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(REGISTRATION_PROFILE, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createDefaultProfile(REGISTRATION_PROFILE, readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_PROFILE, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_PROFILE, readOnlyValidator)));
@ -392,9 +397,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
} }
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern)); readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern));
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators); metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
metadata.addAttribute(UserModel.LOCALE, -1, AbstractUserProfileProvider::isInternationalizationEnabled, AbstractUserProfileProvider::isInternationalizationEnabled)
.setRequired(AttributeMetadata.ALWAYS_FALSE);
return metadata; return metadata;
} }
@ -415,4 +422,13 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
.build(); .build();
} }
private UserProfileMetadata createAccountProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
UserProfileMetadata defaultProfile = createDefaultProfile(context, readOnlyValidator);
defaultProfile.addAttribute(UserModel.LOCALE, -1, AbstractUserProfileProvider::isInternationalizationEnabled, AbstractUserProfileProvider::isInternationalizationEnabled)
.setRequired(AttributeMetadata.ALWAYS_FALSE);
return defaultProfile;
}
} }

View file

@ -48,7 +48,7 @@ public class LegacyAttributes extends DefaultAttributes {
@Override @Override
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) { protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
// user api expects that attributes are not updated if not provided when in legacy mode // user api expects that built-in attributes are not updated if not provided when in legacy mode
return UserProfileContext.USER_API.equals(context); return UserProfileContext.USER_API.equals(context) && !isRootAttribute(metadata.getName());
} }
} }

View file

@ -36,7 +36,9 @@ import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.account.UserProfileAttributeMetadata; import org.keycloak.representations.account.UserProfileAttributeMetadata;
import org.keycloak.representations.account.UserProfileMetadata;
import org.keycloak.representations.account.UserRepresentation; import org.keycloak.representations.account.UserRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@ -368,6 +370,45 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
super.testUpdateSingleField(); super.testUpdateSingleField();
} }
@Test
public void testManageUserLocaleAttribute() throws IOException {
RealmRepresentation realmRep = testRealm().toRepresentation();
Boolean internationalizationEnabled = realmRep.isInternationalizationEnabled();
realmRep.setInternationalizationEnabled(false);
testRealm().update(realmRep);
UserRepresentation user = getUser();
try {
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
user = updateAndGet(user);
assertNull(user.getAttributes().get(UserModel.LOCALE));
realmRep.setInternationalizationEnabled(true);
testRealm().update(realmRep);
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
user = updateAndGet(user);
assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0));
user.getAttributes().remove(UserModel.LOCALE);
user = updateAndGet(user);
assertNull(user.getAttributes().get(UserModel.LOCALE));
UserProfileMetadata metadata = user.getUserProfileMetadata();
assertTrue(metadata.getAttributes().stream()
.map(UserProfileAttributeMetadata::getName)
.filter(UserModel.LOCALE::equals).findAny()
.isPresent()
);
} finally {
realmRep.setInternationalizationEnabled(internationalizationEnabled);
testRealm().update(realmRep);
user.getAttributes().remove(UserModel.LOCALE);
updateAndGet(user);
}
}
protected void setUserProfileConfiguration(String configuration) { protected void setUserProfileConfiguration(String configuration) {
VerifyProfileTest.setUserProfileConfiguration(testRealm(), configuration); VerifyProfileTest.setUserProfileConfiguration(testRealm(), configuration);
} }

View file

@ -8,6 +8,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL; import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
@ -21,6 +22,7 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.ErrorRepresentation;
@ -340,6 +342,34 @@ public class DeclarativeUserTest extends AbstractAdminTest {
}); });
} }
@Test
public void testUserLocale() {
RealmRepresentation realmRep = realm.toRepresentation();
Boolean internationalizationEnabled = realmRep.isInternationalizationEnabled();
realmRep.setInternationalizationEnabled(true);
realm.update(realmRep);
try {
UserRepresentation user1 = new UserRepresentation();
user1.setUsername("user1");
user1.singleAttribute(UserModel.LOCALE, "pt_BR");
String user1Id = createUser(user1);
UserResource userResource = realm.users().get(user1Id);
user1 = userResource.toRepresentation();
assertEquals("pt_BR", user1.getAttributes().get(UserModel.LOCALE).get(0));
realmRep.setInternationalizationEnabled(false);
realm.update(realmRep);
user1 = userResource.toRepresentation();
assertNull(user1.getAttributes().get(UserModel.LOCALE));
} finally {
realmRep.setInternationalizationEnabled(internationalizationEnabled);
realm.update(realmRep);
}
}
private String createUser(UserRepresentation userRep) { private String createUser(UserRepresentation userRep) {
Response response = realm.users().create(userRep); Response response = realm.users().create(userRep);
String createdId = ApiUtil.getCreatedId(response); String createdId = ApiUtil.getCreatedId(response);

View file

@ -0,0 +1,60 @@
/*
* 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.
*/
package org.keycloak.testsuite.i18n;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import jakarta.ws.rs.core.Response;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.openqa.selenium.Cookie;
/**
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
*/
@EnableFeature(Feature.DECLARATIVE_USER_PROFILE)
public class LoginPageWithUserProfileTest extends LoginPageTest {
}

View file

@ -332,7 +332,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
Attributes attributes = profile.getAttributes(); Attributes attributes = profile.getAttributes();
assertThat(attributes.nameSet(), assertThat(attributes.nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address")); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address"));
try { try {
profile.validate(); profile.validate();
@ -400,7 +400,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
Attributes attributes = profile.getAttributes(); Attributes attributes = profile.getAttributes();
assertThat(attributes.nameSet(), assertThat(attributes.nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address", "second")); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address", "second"));
AttributeGroupMetadata companyAddressGroup = attributes.getMetadata("address").getAttributeGroupMetadata(); AttributeGroupMetadata companyAddressGroup = attributes.getMetadata("address").getAttributeGroupMetadata();
@ -517,7 +517,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
UserModel user = profile.create(); UserModel user = profile.create();
assertThat(profile.getAttributes().nameSet(), assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, "address", "department")); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.LOCALE, "address", "department"));
assertNull(user.getFirstAttribute("department")); assertNull(user.getFirstAttribute("department"));
@ -567,7 +567,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
UserModel user = profile.create(); UserModel user = profile.create();
assertThat(profile.getAttributes().nameSet(), assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL)); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.LOCALE));
profile = provider.create(UserProfileContext.USER_API, attributes, user); profile = provider.create(UserProfileContext.USER_API, attributes, user);
@ -609,7 +609,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
UserModel user = profile.create(); UserModel user = profile.create();
assertThat(profile.getAttributes().nameSet(), assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL)); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.LOCALE));
profile = provider.create(UserProfileContext.USER_API, attributes, user); profile = provider.create(UserProfileContext.USER_API, attributes, user);
@ -649,7 +649,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
UserModel user = profile.create(); UserModel user = profile.create();
assertThat(profile.getAttributes().nameSet(), assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, "address", "department", "phone")); containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.LOCALE, "address", "department", "phone"));
profile = provider.create(UserProfileContext.USER_API, attributes, user); profile = provider.create(UserProfileContext.USER_API, attributes, user);