Change locale of user profile validation message to be resolved from authenticated user instead of validated user

Closes #19707
This commit is contained in:
danielFesenmeyer 2023-04-13 20:16:55 +02:00 committed by Pedro Igor
parent feb20de2ef
commit 5554c62bea
3 changed files with 193 additions and 35 deletions

View File

@ -92,7 +92,6 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -186,7 +185,7 @@ public class UserResource {
UserProfile profile = session.getProvider(UserProfileProvider.class).create(USER_API, attributes, user);
Response response = validateUserProfile(profile, user, session);
Response response = validateUserProfile(profile, session, auth.adminAuth());
if (response != null) {
return response;
}
@ -215,9 +214,9 @@ public class UserResource {
logger.warn("Could not update user!", me);
session.getTransactionManager().setRollbackOnly();
throw ErrorResponse.error("Could not update user!", Status.BAD_REQUEST);
} catch (ForbiddenException fe) {
} catch (ForbiddenException | ErrorResponseException e) {
session.getTransactionManager().setRollbackOnly();
throw fe;
throw e;
} catch (Exception me) { // JPA
session.getTransactionManager().setRollbackOnly();
logger.warn("Could not update user!", me);// may be committed by JTA which can't
@ -225,14 +224,15 @@ public class UserResource {
}
}
public static Response validateUserProfile(UserProfile profile, UserModel user, KeycloakSession session) {
public static Response validateUserProfile(UserProfile profile, KeycloakSession session, AdminAuth adminAuth) {
try {
profile.validate();
} catch (ValidationException pve) {
List<ErrorRepresentation> errors = new ArrayList<>();
AdminMessageFormatter adminMessageFormatter = createAdminMessageFormatter(session, adminAuth);
for (ValidationException.Error error : pve.getErrors()) {
errors.add(new ErrorRepresentation(error.getFormattedMessage(new AdminMessageFormatter(session, user))));
errors.add(new ErrorRepresentation(error.getFormattedMessage(adminMessageFormatter)));
}
throw ErrorResponse.errors(errors, Status.BAD_REQUEST);
@ -241,6 +241,13 @@ public class UserResource {
return null;
}
private static AdminMessageFormatter createAdminMessageFormatter(KeycloakSession session, AdminAuth adminAuth) {
// the authenticated user is used to resolve the locale for the messages. It can be null.
UserModel authenticatedUser = adminAuth == null ? null : adminAuth.getUser();
return new AdminMessageFormatter(session, authenticatedUser);
}
public static void updateUserFromRep(UserProfile profile, UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
boolean removeMissingRequiredActions = isUpdateExistingUser;

View File

@ -148,7 +148,7 @@ public class UsersResource {
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
try {
Response response = UserResource.validateUserProfile(profile, null, session);
Response response = UserResource.validateUserProfile(profile, session, auth.adminAuth());
if (response != null) {
return response;
}

View File

@ -1,5 +1,11 @@
package org.keycloak.testsuite.admin;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -7,28 +13,38 @@ import static org.junit.Assert.fail;
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
import static org.keycloak.testsuite.forms.VerifyProfileTest.enableDynamicUserProfile;
import static org.keycloak.testsuite.forms.VerifyProfileTest.setUserProfileConfiguration;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.Profile;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.utils.StringUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfileProvider;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -36,12 +52,20 @@ import org.keycloak.userprofile.UserProfileProvider;
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
public class DeclarativeUserTest extends AbstractAdminTest {
private static final String LOCALE_ATTR_KEY = "locale";
private static final String TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager";
private static final String REQUIRED_ATTR_KEY = "required-attr";
private Keycloak testRealmUserManagerClient;
@Before
public void onBefore() {
RealmRepresentation realmRep = this.realm.toRepresentation();
public void onBefore() throws Exception {
RealmRepresentation realmRep = realm.toRepresentation();
realmRep.setInternationalizationEnabled(true);
realmRep.setSupportedLocales(new HashSet<>(Arrays.asList("en", "de")));
enableDynamicUserProfile(realmRep);
this.realm.update(realmRep);
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
realm.update(realmRep);
setUserProfileConfiguration(realm, "{\"attributes\": ["
+ "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
@ -51,6 +75,50 @@ public class DeclarativeUserTest extends AbstractAdminTest {
+ "{\"name\": \"custom-hidden\"},"
+ "{\"name\": \"attr1\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"attr2\", " + PERMISSIONS_ALL + "}]}");
UserRepresentation testRealmUserManager = UserBuilder.create().username(TEST_REALM_USER_MANAGER_NAME)
.password(TEST_REALM_USER_MANAGER_NAME).build();
String createdUserId = null;
try (Response response = realm.users().create(testRealmUserManager)) {
createdUserId = ApiUtil.getCreatedId(response);
} catch (WebApplicationException e) {
// it's ok when the user has already been created for a previous test
assertThat(e.getResponse().getStatus(), equalTo(409));
}
if (createdUserId != null) {
List<ClientRepresentation> foundClients = realm.clients().findByClientId("realm-management");
assertThat(foundClients, hasSize(1));
ClientRepresentation realmManagementClient = foundClients.get(0);
RoleRepresentation manageUsersRole =
realm.clients().get(realmManagementClient.getId()).roles().get("manage-users").toRepresentation();
assertThat(manageUsersRole, notNullValue());
realm.users().get(createdUserId).roles().clientLevel(realmManagementClient.getId())
.add(Collections.singletonList(manageUsersRole));
}
ClientRepresentation testApp = new ClientRepresentation();
testApp.setClientId("test-app");
testApp.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
testApp.setSecret("secret");
try (Response response = realm.clients().create(testApp)) {
ApiUtil.getCreatedId(response);
} catch (WebApplicationException e) {
// it's ok when the client has already been created for a previous test
assertThat(e.getResponse().getStatus(), equalTo(409));
}
testRealmUserManagerClient = AdminClientUtil.createAdminClient(true, realmRep.getRealm(),
TEST_REALM_USER_MANAGER_NAME, TEST_REALM_USER_MANAGER_NAME, testApp.getClientId(), testApp.getSecret());
}
@After
public void closeClient() {
if (testRealmUserManagerClient != null) {
testRealmUserManagerClient.close();
}
}
@Test
@ -148,12 +216,12 @@ public class DeclarativeUserTest extends AbstractAdminTest {
+ "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"attr1\", \"required\": {}, " + PERMISSIONS_ALL + "}]}");
+ "{\"name\": \"" + REQUIRED_ATTR_KEY + "\", \"required\": {}, " + PERMISSIONS_ALL + "}]}");
UserRepresentation user1 = new UserRepresentation();
user1.setUsername("user1");
// set an attribute to later remove it from the configuration
user1.singleAttribute("attr1", "some-value");
user1.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
String user1Id = createUser(user1);
UserResource userResource = realm.users().get(user1Id);
@ -161,23 +229,106 @@ public class DeclarativeUserTest extends AbstractAdminTest {
user1.setFirstName("changed");
user1.setAttributes(null);
// do not validate attr1 because the attribute list is not provided and the user has the attribute
// do not validate REQUIRED_ATTR_KEY because the attribute list is not provided and the user has the attribute
userResource.update(user1);
user1 = userResource.toRepresentation();
assertEquals("changed", user1.getFirstName());
try {
user1.setAttributes(Collections.emptyMap());
userResource.update(user1);
fail("Should fail because the attribute attr1 is required");
} catch (BadRequestException ignore) {
user1.setAttributes(Collections.emptyMap());
String expectedErrorMessage = String.format("Please specify attribute %s.", REQUIRED_ATTR_KEY);
verifyUserUpdateFails(realm.users(), user1Id, user1, expectedErrorMessage);
}
private void verifyUserUpdateFails(UsersResource usersResource, String userId, UserRepresentation user,
String expectedErrorMessage) {
UserResource userResource = usersResource.get(userId);
try {
userResource.update(user);
fail("Should fail with errorMessage: " + expectedErrorMessage);
} catch (BadRequestException badRequest) {
try (Response response = badRequest.getResponse()) {
assertThat(response.getStatus(), equalTo(400));
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
assertThat(error.getErrorMessage(), equalTo(expectedErrorMessage));
}
}
}
@Test
public void validationErrorMessagesCanBeConfiguredWithRealmLocalization() {
try {
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
+ "{\"name\": \"username\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"firstName\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"email\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"lastName\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"" + LOCALE_ATTR_KEY + "\", " + PERMISSIONS_ALL + "},"
+ "{\"name\": \"" + REQUIRED_ATTR_KEY + "\", \"required\": {}, " + PERMISSIONS_ALL + "}]}");
realm.localization().saveRealmLocalizationText("en", "error-user-attribute-required",
"required-error en: {0}");
getCleanup().addLocalization("en");
realm.localization().saveRealmLocalizationText("de", "error-user-attribute-required",
"required-error de: {0}");
getCleanup().addLocalization("de");
UsersResource testRealmUserManagerClientUsersResource =
testRealmUserManagerClient.realm(REALM_NAME).users();
// start with locale en
changeTestRealmUserManagerLocale("en");
UserRepresentation user = new UserRepresentation();
user.setUsername("user-realm-localization");
user.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
String userId = createUser(user);
user.setAttributes(new HashMap<>());
verifyUserUpdateFails(testRealmUserManagerClientUsersResource, userId, user,
"required-error en: " + REQUIRED_ATTR_KEY);
// switch to locale de
changeTestRealmUserManagerLocale("de");
user.singleAttribute(REQUIRED_ATTR_KEY, "some-value");
realm.users().get(userId).update(user);
user.setAttributes(new HashMap<>());
verifyUserUpdateFails(testRealmUserManagerClientUsersResource, userId, user,
"required-error de: " + REQUIRED_ATTR_KEY);
} finally {
changeTestRealmUserManagerLocale(null);
}
}
private void changeTestRealmUserManagerLocale(String locale) {
UsersResource testRealmUserManagerUsersResource = testRealmUserManagerClient.realm(REALM_NAME).users();
List<UserRepresentation> foundUsers =
testRealmUserManagerUsersResource.search(TEST_REALM_USER_MANAGER_NAME, true);
assertThat(foundUsers, hasSize(1));
UserRepresentation user = foundUsers.iterator().next();
if (locale == null) {
Map<String, List<String>> attributes = user.getAttributes();
if (attributes != null) {
attributes.remove(LOCALE_ATTR_KEY);
}
} else {
user.singleAttribute(LOCALE_ATTR_KEY, locale);
}
// also set REQUIRED_ATTR_KEY, when not already set, otherwise the change will be rejected
if (StringUtil.isBlank(user.firstAttribute(REQUIRED_ATTR_KEY))) {
user.singleAttribute(REQUIRED_ATTR_KEY, "arbitrary-value");
}
testRealmUserManagerUsersResource.get(user.getId()).update(user);
}
@Test
public void testDefaultUserProfileProviderIsActive() {
getTestingClient().server(TEST_REALM_NAME).run(session -> {
getTestingClient().server(REALM_NAME).run(session -> {
Set<UserProfileProvider> providers = session.getAllProviders(UserProfileProvider.class);
assertThat(providers, notNullValue());
assertThat(providers.isEmpty(), is(false));