Add UP decorator to SSSD provider

Closes https://github.com/keycloak/keycloak/issues/25075

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2023-12-01 10:25:16 +01:00 committed by Marek Posolda
parent dc0455b73c
commit 31b7c9d2c3
6 changed files with 536 additions and 84 deletions

View file

@ -33,9 +33,17 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileDecorator;
import org.keycloak.userprofile.UserProfileMetadata;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
@ -49,7 +57,8 @@ public class SSSDFederationProvider implements UserStorageProvider,
UserLookupProvider,
CredentialInputUpdater,
CredentialInputValidator,
ImportedUserValidation {
ImportedUserValidation,
UserProfileDecorator {
private static final Logger logger = Logger.getLogger(SSSDFederationProvider.class);
@ -211,4 +220,40 @@ public class SSSDFederationProvider implements UserStorageProvider,
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
return Stream.empty();
}
@Override
public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) {
// selector by sssd
Predicate<AttributeContext> sssdUsersSelector = attributeContext ->
attributeContext.getUser() != null && model.getId().equals(attributeContext.getUser().getFederationLink());
// condition to view only by admin
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext() == UserProfileContext.USER_API;
// guiOrder if new attributes are needed
int guiOrder = (int) metadata.getAttributes().stream()
.map(AttributeMetadata::getName)
.distinct()
.count();
// firstName, lastName, username and email should be read-only
for (String attrName : List.of(UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL, UserModel.USERNAME)) {
List<AttributeMetadata> attrMetadatas = metadata.getAttribute(attrName);
if (attrMetadatas.isEmpty()) {
logger.debugf("Adding user profile attribute '%s' for sssd provider and context '%s'.", attrName, metadata.getContext());
AttributeMetadata sssdAttrMetadata = metadata.addAttribute(attrName, guiOrder++, Collections.emptyList())
.addWriteCondition(AttributeMetadata.ALWAYS_FALSE)
.addReadCondition(onlyAdminCondition)
.setRequired(AttributeMetadata.ALWAYS_FALSE);
sssdAttrMetadata.setSelector(sssdUsersSelector);
} else {
for (AttributeMetadata attrMetadata : attrMetadatas) {
logger.debugf("Cloning attribute '%s' as read-only for sssd provider and context '%s'.", attrName, metadata.getContext());
AttributeMetadata sssdAttrMetadata = metadata.addAttribute(attrMetadata.clone())
.addWriteCondition(AttributeMetadata.ALWAYS_FALSE);
sssdAttrMetadata.setSelector(sssdUsersSelector);
}
}
}
}
}

View file

@ -20,6 +20,8 @@ package org.keycloak.testsuite.pages;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.testsuite.util.UIUtils;
import org.openqa.selenium.By;
@ -148,6 +150,7 @@ public class LoginUpdateProfilePage extends AbstractPage {
private String lastName;
private String department;
private String email;
private final Map<String, String> other = new LinkedHashMap<>();
protected Update(LoginUpdateProfilePage page) {
this.page = page;
@ -173,6 +176,11 @@ public class LoginUpdateProfilePage extends AbstractPage {
return this;
}
public Update otherProfileAttribute(String name, String value) {
other.put(name, value);
return this;
}
public void submit() {
if (firstName != null) {
page.firstNameInput.clear();
@ -193,6 +201,14 @@ public class LoginUpdateProfilePage extends AbstractPage {
page.emailInput.sendKeys(email);
}
for (Map.Entry<String, String> entry : other.entrySet()) {
WebElement el = page.driver.findElement(By.id(entry.getKey()));
if (el != null) {
el.clear();
el.sendKeys(entry.getValue());
}
}
clickLink(page.submitButton);
}
}

View file

@ -15,6 +15,9 @@
<properties>
<exclude.sssd>**/sssd/**/*Test.java</exclude.sssd>
<maven.compiler.release>11</maven.compiler.release>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
</properties>
<build>

View file

@ -0,0 +1,125 @@
/*
* Copyright 2023 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.sssd;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.util.OAuthClient;
/**
* <p>Abstract base class for SSSD tests.</p>
*
* @author rmartinc
*/
public abstract class AbstractBaseSSSDTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPage loginPage;
@Page
protected AppPage appPage;
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected LoginUpdateProfilePage updateProfilePage;
// vars for the configuration of the SSSD installation
protected static final String PROVIDER_NAME = "sssd";
private static final String sssdConfigPath = "sssd/sssd.properties";
private static PropertiesConfiguration sssdConfig;
protected static final String DISABLED_USER = "disabled";
protected static final String NO_EMAIL_USER = "noemail";
protected static final String ADMIN_USER = "admin";
@BeforeClass
public static void loadSSSDConfiguration() throws ConfigurationException {
InputStream is = SSSDTest.class.getClassLoader().getResourceAsStream(sssdConfigPath);
sssdConfig = new PropertiesConfiguration();
sssdConfig.load(is);
sssdConfig.setListDelimiter(',');
}
protected void testLoginFailure(String username, String password) {
loginPage.open();
loginPage.login(username, password);
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
events.expect(EventType.LOGIN_ERROR).user(Matchers.any(String.class)).error(Errors.INVALID_USER_CREDENTIALS).assertEvent();
}
protected void testLoginSuccess(String username) {
loginPage.open();
loginPage.login(username, getPassword(username));
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin().user(Matchers.any(String.class))
.detail(Details.USERNAME, username).assertEvent();
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
}
protected String getUsername() {
return sssdConfig.getStringArray("usernames")[0];
}
protected String getFirstName(String username) {
return sssdConfig.getString("user." + username + ".firstname");
}
protected String getLastName(String username) {
return sssdConfig.getString("user." + username + ".lastname");
}
protected String getEmail(String username) {
return sssdConfig.getString("user." + username + ".mail");
}
protected String getUser(String type) {
return sssdConfig.getString("user." + type);
}
protected List<String> getUsernames() {
return Arrays.asList(sssdConfig.getStringArray("usernames"));
}
protected String getPassword(String username) {
return sssdConfig.getString("user." + username + ".password");
}
protected List<String> getGroups(String username) {
return Arrays.asList(sssdConfig.getStringArray("user." + username + ".groups"));
}
}

View file

@ -1,44 +1,43 @@
/*
* Copyright 2023 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.sssd;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.OAuthClient;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@ -61,45 +60,19 @@ import static org.hamcrest.Matchers.greaterThan;
*
* @author rmartinc
*/
public class SSSDTest extends AbstractTestRealmKeycloakTest {
public class SSSDTest extends AbstractBaseSSSDTest {
private static final Logger log = Logger.getLogger(SSSDTest.class);
private static final String DISPLAY_NAME = "Test user federation";
private static final String PROVIDER_NAME = "sssd";
private static final String REALM_NAME = "test";
private static final String sssdConfigPath = "sssd/sssd.properties";
private static final String DISABLED_USER = "disabled";
private static final String NO_EMAIL_USER = "noemail";
private static final String ADMIN_USER = "admin";
private static PropertiesConfiguration sssdConfig;
@Page
protected LoginPage loginPage;
@Page
protected AppPage appPage;
@Rule
public AssertEvents events = new AssertEvents(this);
private String SSSDFederationID;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@BeforeClass
public static void loadSSSDConfiguration() throws ConfigurationException {
log.info("Reading SSSD configuration from classpath from: " + sssdConfigPath);
InputStream is = SSSDTest.class.getClassLoader().getResourceAsStream(sssdConfigPath);
sssdConfig = new PropertiesConfiguration();
sssdConfig.load(is);
sssdConfig.setListDelimiter(',');
}
@Before
public void createUserFederation() {
ComponentRepresentation userFederation = new ComponentRepresentation();
@ -117,25 +90,6 @@ public class SSSDTest extends AbstractTestRealmKeycloakTest {
}
}
private void testLoginFailure(String username, String password) {
loginPage.open();
loginPage.login(username, password);
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
events.expect(EventType.LOGIN_ERROR).user(Matchers.any(String.class)).error(Errors.INVALID_USER_CREDENTIALS).assertEvent();
}
private void testLoginSuccess(String username) {
loginPage.open();
loginPage.login(username, getPassword(username));
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin().user(Matchers.any(String.class))
.detail(Details.USERNAME, username).assertEvent();
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
}
@Test
public void testInvalidPassword() {
String username = getUsername();
@ -238,24 +192,4 @@ public class SSSDTest extends AbstractTestRealmKeycloakTest {
List<String> assignedGroupNames = assignedGroups.stream().map(GroupRepresentation::getName).collect(Collectors.toList());
MatcherAssert.assertThat(assignedGroupNames, Matchers.hasItems(groups.toArray(new String[0])));
}
private String getUsername() {
return sssdConfig.getStringArray("usernames")[0];
}
private String getUser(String type) {
return sssdConfig.getString("user." + type);
}
private List<String> getUsernames() {
return Arrays.asList(sssdConfig.getStringArray("usernames"));
}
private String getPassword(String username) {
return sssdConfig.getString("user." + username + ".password");
}
private List<String> getGroups(String username) {
return Arrays.asList(sssdConfig.getStringArray("user." + username + ".groups"));
}
}

View file

@ -0,0 +1,329 @@
/*
* Copyright 2023 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.sssd;
import java.util.List;
import java.util.Set;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserProfileAttributeMetadata;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.userprofile.config.UPConfigUtils;
/**
* <p>Test for the User profile integration in the SSSD provider.</p>
*
* @author rmartinc
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
public class SSSDUserProfileTest extends AbstractBaseSSSDTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
// enable user profile and add sssd provider in the realm
VerifyProfileTest.enableDynamicUserProfile(testRealm);
ComponentExportRepresentation sssdComp = new ComponentExportRepresentation();
sssdComp.setName(PROVIDER_NAME);
sssdComp.setProviderId(PROVIDER_NAME);
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle("cachePolicy", "DEFAULT");
config.putSingle("enabled", "true");
sssdComp.setConfig(config);
MultivaluedHashMap<String, ComponentExportRepresentation> components = testRealm.getComponents();
if (components == null) {
components = new MultivaluedHashMap<>();
testRealm.setComponents(components);
}
components.add(UserStorageProvider.class.getName(), sssdComp);
}
@Test
public void test01LoginSuccess() throws Exception {
// do a login to create the first sssd user in the configuration
testLoginSuccess(getUsername());
}
@Test
public void test02DefaultSSSDUserProfile() throws Exception {
// default configuration adds all sssd attributes
// check they are read-only in both admin and user for a SSSD user
String username = getUsername();
UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), username);
UserRepresentation user = userResource.toRepresentation(true);
// for admin the four should be read-only
String sssdId = getSssdProviderId();
assertUser(user, username, getEmail(username), getFirstName(username), getLastName(username), sssdId);
assertProfileAttributes(user, null, true, UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME);
user.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
userResource.update(user);
// for user the same, the four attrs should be read-only
loginPage.open();
loginPage.login(username, getPassword(username));
WaitUtils.waitForPageToLoad();
updateProfilePage.assertCurrent();
Assert.assertEquals(getFirstName(username), updateProfilePage.getFirstName());
Assert.assertEquals(getLastName(username), updateProfilePage.getLastName());
Assert.assertEquals(getEmail(username), updateProfilePage.getEmail());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.FIRST_NAME).isEnabled());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.LAST_NAME).isEnabled());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
updateProfilePage.prepareUpdate().submit();
// check events
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
events.expectRequiredAction(EventType.UPDATE_PROFILE)
.user(user.getId())
.assertEvent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin()
.user(Matchers.any(String.class))
.detail(Details.USERNAME, username)
.assertEvent();
// logout
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
}
@Test
public void test03DefaultInternalDBUserProfile() throws Exception {
// check non sssd user has normal atttributes enabled
UserResource testResource = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
UserRepresentation test = testResource.toRepresentation(true);
// for admin the four should be editable
assertUser(test, "test-user@localhost", "test-user@localhost", "Tom", "Brady", null);
assertProfileAttributes(test, null, false, UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME);
test.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
testResource.update(test);
// for user the same, the four attrs should be editable
loginPage.open();
loginPage.login("test-user@localhost", "password");
WaitUtils.waitForPageToLoad();
updateProfilePage.assertCurrent();
Assert.assertEquals("Tom", updateProfilePage.getFirstName());
Assert.assertEquals("Brady", updateProfilePage.getLastName());
Assert.assertEquals("test-user@localhost", updateProfilePage.getEmail());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.FIRST_NAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.LAST_NAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
updateProfilePage.prepareUpdate().submit();
// check events
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
events.expectRequiredAction(EventType.UPDATE_PROFILE)
.user(test.getId())
.assertEvent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin()
.user(Matchers.any(String.class))
.detail(Details.USERNAME, "test-user@localhost")
.assertEvent();
// logout
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
}
@Test
public void test04MixedSSSDUserProfile() throws Exception {
RealmResource realm = testRealm();
UPConfig origConfig = realm.users().userProfile().getConfiguration();
try {
createMixedUPConfiguration();
// for admin all attributes are added as read-only and postal_code remains editable
String username = getUsername();
String sssdId = getSssdProviderId();
UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), username);
UserRepresentation user = userResource.toRepresentation(true);
assertUser(user, username, getEmail(username), getFirstName(username), getLastName(username), sssdId);
assertProfileAttributes(user, null, true, UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME);
assertProfileAttributes(user, null, false, "postal_code");
// for user, firstName and lastName are not visible, username and email read-only, postal_code editable
user.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
userResource.update(user);
loginPage.open();
loginPage.login(username, getPassword(username));
WaitUtils.waitForPageToLoad();
updateProfilePage.assertCurrent();
Assert.assertEquals(getEmail(username), updateProfilePage.getEmail());
Assert.assertNull(updateProfilePage.getFieldById(UserModel.FIRST_NAME));
Assert.assertNull(updateProfilePage.getFieldById(UserModel.LAST_NAME));
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertFalse(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById("postal_code").isEnabled());
updateProfilePage.prepareUpdate().otherProfileAttribute("postal_code", "123456").submit();
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
// check events
events.expectRequiredAction(EventType.UPDATE_PROFILE)
.user(user.getId())
.detail("updated_postal_code", "123456")
.assertEvent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin()
.user(Matchers.any(String.class))
.detail(Details.USERNAME, username)
.assertEvent();
// logout
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
} finally {
realm.users().userProfile().update(origConfig);
}
}
@Test
public void test05MixedInternalDBUserProfile() throws Exception {
RealmResource realm = testRealm();
UPConfig origConfig = realm.users().userProfile().getConfiguration();
try {
createMixedUPConfiguration();
// for admin firstName and lastName remains removed, the rest editable
UserResource testResource = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
UserRepresentation test = testResource.toRepresentation(true);
assertUser(test, "test-user@localhost", "test-user@localhost", "Tom", "Brady", null);
assertProfileAttributes(test, null, false, "username", "email", "postal_code");
Assert.assertNull(test.getUserProfileMetadata().getAttributeMetadata(UserModel.FIRST_NAME));
Assert.assertNull(test.getUserProfileMetadata().getAttributeMetadata(UserModel.LAST_NAME));
// for user, firstName and lastName are not visible, username, email read-only and postal_code editable
test.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
testResource.update(test);
loginPage.open();
loginPage.login("test-user@localhost", "password");
WaitUtils.waitForPageToLoad();
updateProfilePage.assertCurrent();
Assert.assertEquals("test-user@localhost", updateProfilePage.getEmail());
Assert.assertNull(updateProfilePage.getFieldById(UserModel.FIRST_NAME));
Assert.assertNull(updateProfilePage.getFieldById(UserModel.LAST_NAME));
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.EMAIL).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById(UserModel.USERNAME).isEnabled());
Assert.assertTrue(updateProfilePage.getFieldById("postal_code").isEnabled());
updateProfilePage.prepareUpdate().otherProfileAttribute("postal_code", "123456").submit();
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
// check events
events.expectRequiredAction(EventType.UPDATE_PROFILE)
.user(test.getId())
.detail("updated_postal_code", "123456")
.assertEvent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
EventRepresentation loginEvent = events.expectLogin()
.user(Matchers.any(String.class))
.detail(Details.USERNAME, "test-user@localhost")
.assertEvent();
// logout
OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
appPage.logout(tokenResponse.getIdToken());
events.expectLogout(loginEvent.getSessionId()).user(loginEvent.getUserId()).assertEvent();
} finally {
realm.users().userProfile().update(origConfig);
}
}
private void assertUser(UserRepresentation user, String expectedUsername, String expectedEmail,
String expectedFirstName, String expectedLastname, String sssdId) {
Assert.assertNotNull(user);
Assert.assertEquals(expectedUsername, user.getUsername());
Assert.assertEquals(expectedFirstName, user.getFirstName());
Assert.assertEquals(expectedLastname, user.getLastName());
Assert.assertEquals(expectedEmail, user.getEmail());
Assert.assertEquals(sssdId, user.getFederationLink());
}
private void assertProfileAttributes(UserRepresentation user, String expectedGroup, boolean expectReadOnly, String... attributes) {
for (String attrName : attributes) {
UserProfileAttributeMetadata attrMetadata = user.getUserProfileMetadata().getAttributeMetadata(attrName);
Assert.assertNotNull("Attribute " + attrName + " was not present for user " + user.getUsername(), attrMetadata);
Assert.assertEquals("Attribute " + attrName + " for user " + user.getUsername() + ". Expected read-only: " + expectReadOnly + " but was not", expectReadOnly, attrMetadata.isReadOnly());
Assert.assertEquals("Attribute " + attrName + " for user " + user.getUsername() + ". Expected group: " + expectedGroup + " but was " + attrMetadata.getGroup(), expectedGroup, attrMetadata.getGroup());
}
}
private String getSssdProviderId() {
List<ComponentRepresentation> comps = testRealm().components()
.query(TEST_REALM_NAME, UserStorageProvider.class.getName(), PROVIDER_NAME);
Assert.assertEquals(1, comps.size());
return comps.iterator().next().getId();
}
private void createMixedUPConfiguration() {
// removes firstName and lastName, adds a custom postal_code
RealmResource realm = testRealm();
UPConfig config = realm.users().userProfile().getConfiguration();
config.getAttributes().remove(config.getAttribute(UserModel.FIRST_NAME));
config.getAttributes().remove(config.getAttribute(UserModel.LAST_NAME));
UPAttribute postalCode = new UPAttribute();
postalCode.setName("postal_code");
postalCode.setDisplayName("Postal Code");
UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setView(Set.of(UPConfigUtils.ROLE_USER, UPConfigUtils.ROLE_ADMIN));
permissions.setEdit(Set.of(UPConfigUtils.ROLE_USER, UPConfigUtils.ROLE_ADMIN));
postalCode.setPermissions(permissions);
UPAttributeRequired required = new UPAttributeRequired();
required.setRoles(Set.of(UPConfigUtils.ROLE_USER));
postalCode.setRequired(required);
config.getAttributes().add(postalCode);
realm.users().userProfile().update(config);
}
}