User profile configuration scoped to user-federation provider
closes #23878 Co-Authored-By: mposolda <mposolda@gmail.com> Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
040124bfa7
commit
2c611cb8fc
18 changed files with 564 additions and 6 deletions
|
@ -18,8 +18,10 @@
|
|||
package org.keycloak.storage.ldap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -60,6 +62,7 @@ import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
|
|||
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
||||
import org.keycloak.policy.PolicyError;
|
||||
import org.keycloak.models.cache.UserCache;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.storage.DatastoreProvider;
|
||||
import org.keycloak.storage.LegacyStoreManagers;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
|
@ -86,6 +89,14 @@ import org.keycloak.storage.user.ImportedUserValidation;
|
|||
import org.keycloak.storage.user.UserLookupProvider;
|
||||
import org.keycloak.storage.user.UserQueryMethodsProvider;
|
||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||
import org.keycloak.userprofile.AbstractUserProfileProvider;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeGroupMetadata;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileDecorator;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||
|
||||
|
@ -101,7 +112,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
UserLookupProvider,
|
||||
UserRegistrationProvider,
|
||||
UserQueryMethodsProvider,
|
||||
ImportedUserValidation {
|
||||
ImportedUserValidation,
|
||||
UserProfileDecorator {
|
||||
private static final Logger logger = Logger.getLogger(LDAPStorageProvider.class);
|
||||
private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE >> 1;
|
||||
|
||||
|
@ -963,4 +975,93 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
public String toString() {
|
||||
return "LDAPStorageProvider - " + getModel().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) {
|
||||
Predicate<AttributeContext> ldapUsersSelector = (attributeContext -> {
|
||||
UserModel user = attributeContext.getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (model.isImportEnabled()) {
|
||||
return getModel().getId().equals(user.getFederationLink());
|
||||
} else {
|
||||
return getModel().getId().equals(new StorageId(user.getId()).getProviderId());
|
||||
}
|
||||
});
|
||||
|
||||
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext() == UserProfileContext.USER_API;
|
||||
|
||||
int guiOrder = (int) metadata.getAttributes().stream()
|
||||
.map(AttributeMetadata::getName)
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
// 1 - get configured attributes from LDAP mappers and add them to the user profile (if they not already present)
|
||||
Set<String> attributes = new LinkedHashSet<>();
|
||||
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
|
||||
.sorted(ldapMappersComparator.sortAsc())
|
||||
.forEachOrdered(mapperModel -> {
|
||||
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
|
||||
attributes.addAll(ldapMapper.getUserAttributes());
|
||||
});
|
||||
for (String attrName : attributes) {
|
||||
// In case that attributes from LDAP mappers are explicitly defined on user profile, we can prefer defined configuration
|
||||
if (!metadata.getAttribute(attrName).isEmpty()) {
|
||||
logger.debugf("Ignore adding attribute '%s' to user profile by LDAP provider '%s' as attribute is already defined on user profile.", attrName, getModel().getName());
|
||||
} else {
|
||||
logger.debugf("Adding attribute '%s' to user profile by LDAP provider '%s' for user profile context '%s'.", attrName, getModel().getName(), metadata.getContext().toString());
|
||||
// Writable and readable only by administrators by default. Applied only for LDAP users
|
||||
AttributeMetadata attributeMetadata = metadata.addAttribute(attrName, guiOrder++, Collections.emptyList())
|
||||
.addWriteCondition(onlyAdminCondition)
|
||||
.addReadCondition(onlyAdminCondition)
|
||||
.setRequired(AttributeMetadata.ALWAYS_FALSE);
|
||||
attributeMetadata.setSelector(ldapUsersSelector);
|
||||
}
|
||||
}
|
||||
|
||||
// 2 - metadata attributes
|
||||
Set<String> metadataAttributes = new HashSet<>(List.of(LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN));
|
||||
if (getKerberosConfig().isAllowKerberosAuthentication()) {
|
||||
metadataAttributes.add(KerberosConstants.KERBEROS_PRINCIPAL);
|
||||
}
|
||||
|
||||
AttributeGroupMetadata metadataGroup = lookupMetadataGroup();
|
||||
|
||||
for (String attrName : metadataAttributes) {
|
||||
// In case that attributes like LDAP_ID, KERBEROS_PRINCIPAL are explicitly defined on user profile, we can prefer defined configuration
|
||||
if (!metadata.getAttribute(attrName).isEmpty()) {
|
||||
logger.debugf("Ignore adding metadata attribute '%s' to user profile by LDAP provider '%s' as attribute is already defined on user profile.", attrName, getModel().getName());
|
||||
} else {
|
||||
logger.debugf("Adding metadata attribute '%s' to user profile by LDAP provider '%s' for user profile context '%s'.", attrName, getModel().getName(), metadata.getContext().toString());
|
||||
AttributeMetadata attributeMetadata = metadata.addAttribute(attrName, guiOrder++, Collections.emptyList())
|
||||
.addWriteCondition(AttributeMetadata.ALWAYS_FALSE) // Not writable for anyone
|
||||
.addReadCondition(onlyAdminCondition) // Read-only for administrators
|
||||
.setRequired(AttributeMetadata.ALWAYS_FALSE);
|
||||
|
||||
if (metadataGroup != null) {
|
||||
attributeMetadata.setAttributeGroupMetadata(metadataGroup);
|
||||
}
|
||||
attributeMetadata.setSelector(ldapUsersSelector);
|
||||
}
|
||||
}
|
||||
|
||||
// 3 - make all attributes read-only for LDAP users in case that LDAP itself is read-only
|
||||
if (getEditMode() == EditMode.READ_ONLY) {
|
||||
for (AttributeMetadata attrMetadata : metadata.getAttributes()) {
|
||||
attrMetadata.addWriteCondition(ldapUsersSelector.negate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AttributeGroupMetadata lookupMetadataGroup() {
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig config = provider.getConfiguration();
|
||||
return config.getGroups().stream()
|
||||
.filter(upGroup -> AbstractUserProfileProvider.USER_METADATA_GROUP.equals(upGroup.getName()))
|
||||
.map(upGroup -> new AttributeGroupMetadata(upGroup.getName(), upGroup.getDisplayHeader(), upGroup.getDisplayDescription(), upGroup.getAnnotations()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,11 @@ public abstract class AbstractLDAPStorageMapper implements LDAPStorageMapper {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getUserAttributes() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public static boolean parseBooleanParameter(ComponentModel mapperModel, String paramName) {
|
||||
String paramm = mapperModel.getConfig().getFirst(paramName);
|
||||
return Boolean.parseBoolean(paramm);
|
||||
|
|
|
@ -235,6 +235,11 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
query.addWhereCondition(fullNameCondition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getUserAttributes() {
|
||||
return new HashSet<>(List.of(UserModel.FIRST_NAME, UserModel.LAST_NAME));
|
||||
}
|
||||
|
||||
protected String getLdapFullNameAttrName() {
|
||||
String ldapFullNameAttrName = mapperModel.getConfig().getFirst(LDAP_FULL_NAME_ATTRIBUTE);
|
||||
return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
|
||||
|
|
|
@ -94,6 +94,13 @@ public interface LDAPStorageMapper extends Provider {
|
|||
*/
|
||||
Set<String> mandatoryAttributeNames();
|
||||
|
||||
/**
|
||||
* Method that returns user model attributes, which this mapper maps to Keycloak users
|
||||
*
|
||||
* @return user model attributes. Returns empty set if not user attributes provided by this mapper. Never returns null.
|
||||
*/
|
||||
Set<String> getUserAttributes();
|
||||
|
||||
/**
|
||||
* Called when invoke proxy on LDAP federation provider
|
||||
*
|
||||
|
|
|
@ -151,6 +151,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
return isMandatoryInLdap? Collections.singleton(getLdapAttributeName()) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getUserAttributes() {
|
||||
return Collections.singleton(getUserModelAttribute());
|
||||
}
|
||||
|
||||
// throw ModelDuplicateException if there is different user in model with same email
|
||||
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
|
||||
if (email == null || realm.isDuplicateEmailsAllowed()) return;
|
||||
|
|
|
@ -63,6 +63,8 @@ import org.keycloak.storage.StorageId;
|
|||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.client.ClientStorageProvider;
|
||||
import org.keycloak.userprofile.UserProfileDecorator;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -78,7 +80,7 @@ import java.util.stream.Stream;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateComponent {
|
||||
public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateComponent, UserProfileDecorator {
|
||||
protected static final Logger logger = Logger.getLogger(UserCacheSession.class);
|
||||
protected UserCacheManager cache;
|
||||
protected KeycloakSession session;
|
||||
|
@ -946,4 +948,11 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
|||
((OnCreateComponent) getDelegate()).onCreate(session, realm, model);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) {
|
||||
if (getDelegate() instanceof UserProfileDecorator) {
|
||||
((UserProfileDecorator) getDelegate()).decorateUserProfile(realm, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,13 +70,15 @@ import org.keycloak.storage.user.UserLookupProvider;
|
|||
import org.keycloak.storage.user.UserQueryMethodsProvider;
|
||||
import org.keycloak.storage.user.UserQueryProvider;
|
||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||
import org.keycloak.userprofile.UserProfileDecorator;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserStorageManager extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel>
|
||||
implements UserProvider, OnUserCache, OnCreateComponent, OnUpdateComponent {
|
||||
implements UserProvider, OnUserCache, OnCreateComponent, OnUpdateComponent, UserProfileDecorator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(UserStorageManager.class);
|
||||
|
||||
|
@ -786,4 +788,12 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata) {
|
||||
for (UserProfileDecorator decorator : getEnabledStorageProviders(session.getContext().getRealm(), UserProfileDecorator.class)
|
||||
.collect(Collectors.toList())) {
|
||||
decorator.decorateUserProfile(realm, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,12 @@ import java.util.function.Consumer;
|
|||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.CollectionUtil;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.storage.StorageId;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
|
@ -224,7 +226,9 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
for (String name : nameSet()) {
|
||||
AttributeMetadata metadata = getMetadata(name);
|
||||
|
||||
if (metadata == null || !metadata.canView(createAttributeContext(metadata))) {
|
||||
if (metadata == null
|
||||
|| !metadata.canView(createAttributeContext(metadata))
|
||||
|| !metadata.isSelected(createAttributeContext(metadata))) {
|
||||
attributes.remove(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -296,4 +296,5 @@ public interface UserProvider extends Provider,
|
|||
* @param component the component model
|
||||
*/
|
||||
void preRemove(RealmModel realm, ComponentModel component);
|
||||
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public final class AttributeMetadata {
|
|||
private final String attributeName;
|
||||
private String attributeDisplayName;
|
||||
private AttributeGroupMetadata attributeGroupMetadata;
|
||||
private final Predicate<AttributeContext> selector;
|
||||
private Predicate<AttributeContext> selector;
|
||||
private final List<Predicate<AttributeContext>> writeAllowed = new ArrayList<>();
|
||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||
private Predicate<AttributeContext> required;
|
||||
|
@ -131,6 +131,10 @@ public final class AttributeMetadata {
|
|||
return selector.test(context);
|
||||
}
|
||||
|
||||
public void setSelector(Predicate<AttributeContext> selector) {
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
private boolean allConditionsMet(List<Predicate<AttributeContext>> predicates, AttributeContext context) {
|
||||
return predicates.stream().allMatch(p -> p.test(context));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.userprofile;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface UserProfileDecorator {
|
||||
|
||||
/**
|
||||
* Decorates user profile with additional metadata. For instance, metadata attributes, which are available just for your user-storage
|
||||
* provider can be added there, so they are available just for the users coming from your provider
|
||||
*
|
||||
* @param metadata to decorate
|
||||
*/
|
||||
void decorateUserProfile(RealmModel realm, UserProfileMetadata metadata);
|
||||
}
|
|
@ -73,6 +73,8 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
public static final String CONFIG_READ_ONLY_ATTRIBUTES = "read-only-attributes";
|
||||
public static final String MAX_EMAIL_LOCAL_PART_LENGTH = "max-email-local-part-length";
|
||||
|
||||
public static final String USER_METADATA_GROUP = "user-metadata";
|
||||
|
||||
private static boolean editUsernameCondition(AttributeContext c) {
|
||||
KeycloakSession session = c.getSession();
|
||||
KeycloakContext context = session.getContext();
|
||||
|
|
|
@ -46,11 +46,13 @@ import org.keycloak.models.ClientModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.storage.DatastoreProvider;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
|
@ -170,6 +172,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
ComponentModel component = getComponentModel().orElse(null);
|
||||
|
||||
if (component == null) {
|
||||
// makes sure user providers can override metadata for any attribute
|
||||
decorateUserProfileMetadataWithUserStorage(realm, decoratedMetadata);
|
||||
return decoratedMetadata;
|
||||
}
|
||||
|
||||
|
@ -429,10 +433,23 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
}
|
||||
}
|
||||
|
||||
if (session != null) {
|
||||
// makes sure user providers can override metadata for any attribute
|
||||
decorateUserProfileMetadataWithUserStorage(session.getContext().getRealm(), decoratedMetadata);
|
||||
}
|
||||
|
||||
return decoratedMetadata;
|
||||
|
||||
}
|
||||
|
||||
private void decorateUserProfileMetadataWithUserStorage(RealmModel realm, UserProfileMetadata userProfileMetadata) {
|
||||
// makes sure user providers can override metadata for any attribute
|
||||
UserProvider users = session.users();
|
||||
if (users instanceof UserProfileDecorator) {
|
||||
((UserProfileDecorator) users).decorateUserProfile(realm, userProfileMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, UPGroup> asHashMap(List<UPGroup> groups) {
|
||||
return groups.stream().collect(Collectors.toMap(g -> g.getName(), g -> g));
|
||||
}
|
||||
|
|
|
@ -52,5 +52,12 @@
|
|||
"person-name-prohibited-characters": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"name": "user-metadata",
|
||||
"displayHeader": "User metadata",
|
||||
"displayDescription": "Attributes, which refer to user metadata"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -111,6 +111,14 @@ public class LoginUpdateProfilePage extends AbstractPage {
|
|||
public String getLabelForField(String fieldId) {
|
||||
return driver.findElement(By.cssSelector("label[for="+fieldId+"]")).getText();
|
||||
}
|
||||
|
||||
public WebElement getFieldById(String fieldId) {
|
||||
try {
|
||||
return driver.findElement(By.id(fieldId));
|
||||
} catch (NoSuchElementException nsee) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDepartmentPresent() {
|
||||
try {
|
||||
|
|
|
@ -201,7 +201,11 @@ public class UserProfileAdminTest extends AbstractAdminTest {
|
|||
assertEquals(group.getName(), mGroup.getName());
|
||||
assertEquals(group.getDisplayHeader(), mGroup.getDisplayHeader());
|
||||
assertEquals(group.getDisplayDescription(), mGroup.getDisplayDescription());
|
||||
assertEquals(group.getAnnotations().size(), mGroup.getAnnotations().size());
|
||||
if (group.getAnnotations() == null) {
|
||||
assertEquals(group.getAnnotations(), mGroup.getAnnotations());
|
||||
} else {
|
||||
assertEquals(group.getAnnotations().size(), mGroup.getAnnotations().size());
|
||||
}
|
||||
}
|
||||
assertEquals(config.getGroups().get(0).getName(), metadata.getAttributeMetadata(UserModel.FIRST_NAME).getGroup());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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.federation.ldap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
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.UPConfig;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.testsuite.ProfileAssume;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.forms.VerifyProfileTest;
|
||||
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
|
||||
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
|
||||
import static org.keycloak.userprofile.AbstractUserProfileProvider.USER_METADATA_GROUP;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
|
||||
public class LDAPUserProfileTest extends AbstractLDAPTest {
|
||||
|
||||
@ClassRule
|
||||
public static LDAPRule ldapRule = new LDAPRule();
|
||||
|
||||
@Page
|
||||
protected LoginUpdateProfilePage updateProfilePage;
|
||||
|
||||
@Override
|
||||
protected LDAPRule getLDAPRule() {
|
||||
return ldapRule;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
// don't run this test when map storage is enabled, as map storage doesn't support LDAP, yet
|
||||
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterImportTestRealm() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
UserModel user = LDAPTestUtils.addLocalUser(session, appRealm, "marykeycloak", "mary@test.com", "Password1");
|
||||
user.setFirstName("Mary");
|
||||
user.setLastName("Kelly");
|
||||
|
||||
LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ctx.getLdapModel());
|
||||
|
||||
// Delete all LDAP users and add some new for testing
|
||||
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
|
||||
|
||||
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
|
||||
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
|
||||
|
||||
LDAPObject john2 = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak2", "John", "Doe", "john2@email.org", null, "1234");
|
||||
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john2, "Password1");
|
||||
});
|
||||
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
VerifyProfileTest.enableDynamicUserProfile(realm);
|
||||
testRealm().update(realm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserProfile() {
|
||||
// Test user profile of user johnkeycloak in admin API
|
||||
UserResource johnResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak");
|
||||
UserRepresentation john = johnResource.toRepresentation(true);
|
||||
|
||||
assertUser(john, "johnkeycloak", "john@email.org", "John", "Doe", "1234");
|
||||
assertProfileAttributes(john, null, false, "username", "email", "firstName", "lastName", "postal_code");
|
||||
assertProfileAttributes(john, USER_METADATA_GROUP, true, LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN);
|
||||
|
||||
// Test Update profile
|
||||
john.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
|
||||
johnResource.update(john);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("johnkeycloak", "Password1");
|
||||
updateProfilePage.assertCurrent();
|
||||
Assert.assertEquals("John", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("Doe", updateProfilePage.getLastName());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("firstName").isEnabled());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("lastName").isEnabled());
|
||||
Assert.assertNull(updateProfilePage.getFieldById("postal_code"));
|
||||
updateProfilePage.prepareUpdate().submit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserProfileWithDefinedAttribute() throws IOException {
|
||||
UPConfig origConfig = testRealm().users().userProfile().getConfiguration();
|
||||
try {
|
||||
UPConfig config = testRealm().users().userProfile().getConfiguration();
|
||||
// Set postal code
|
||||
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);
|
||||
config.getAttributes().add(postalCode);
|
||||
testRealm().users().userProfile().update(config);
|
||||
|
||||
// Defined postal_code in user profile config should have preference
|
||||
UserResource johnResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak");
|
||||
UserRepresentation john = johnResource.toRepresentation(true);
|
||||
Assert.assertEquals("Postal Code", john.getUserProfileMetadata().getAttributeMetadata("postal_code").getDisplayName());
|
||||
|
||||
// update profile now.
|
||||
john.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
|
||||
johnResource.update(john);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("johnkeycloak", "Password1");
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
Assert.assertEquals("John", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("Doe", updateProfilePage.getLastName());
|
||||
Assert.assertEquals("1234", updateProfilePage.getFieldById("postal_code").getAttribute("value"));
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("firstName").isEnabled());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("lastName").isEnabled());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("postal_code").isEnabled());
|
||||
updateProfilePage.prepareUpdate().submit();
|
||||
} finally {
|
||||
testRealm().users().userProfile().update(origConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserProfileWithReadOnlyLdap() {
|
||||
// Test user profile of user johnkeycloak in admin console as well as account console. Check attributes are writable.
|
||||
setLDAPReadOnly();
|
||||
try {
|
||||
UserResource johnResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak");
|
||||
UserRepresentation john = johnResource.toRepresentation(true);
|
||||
|
||||
assertProfileAttributes(john, null, true, "username", "email", "firstName", "lastName", "postal_code");
|
||||
assertProfileAttributes(john, USER_METADATA_GROUP, true, LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN);
|
||||
|
||||
// Test Update profile. Fields are read only
|
||||
john.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
|
||||
johnResource.update(john);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("johnkeycloak", "Password1");
|
||||
updateProfilePage.assertCurrent();
|
||||
Assert.assertEquals("John", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("Doe", updateProfilePage.getLastName());
|
||||
Assert.assertFalse(updateProfilePage.getFieldById("firstName").isEnabled());
|
||||
Assert.assertFalse(updateProfilePage.getFieldById("lastName").isEnabled());
|
||||
Assert.assertNull(updateProfilePage.getFieldById("postal_code"));
|
||||
updateProfilePage.prepareUpdate().submit();
|
||||
} finally {
|
||||
setLDAPWritable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserProfileWithReadOnlyLdapLocalUser() {
|
||||
// Test local user is writable and has only attributes defined explicitly in user-profile
|
||||
setLDAPReadOnly();
|
||||
try {
|
||||
UserResource maryResource = ApiUtil.findUserByUsernameId(testRealm(), "marykeycloak");
|
||||
UserRepresentation mary = maryResource.toRepresentation(true);
|
||||
|
||||
// LDAP is read-only, but local user has all the attributes writable
|
||||
assertProfileAttributes(mary, null, false, "username", "email", "firstName", "lastName");
|
||||
assertProfileAttributesNotPresent(mary, "postal_code", LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN);
|
||||
|
||||
// Test Update profile
|
||||
mary.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PROFILE.toString());
|
||||
maryResource.update(mary);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("marykeycloak", "Password1");
|
||||
updateProfilePage.assertCurrent();
|
||||
Assert.assertEquals("Mary", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("Kelly", updateProfilePage.getLastName());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("firstName").isEnabled());
|
||||
Assert.assertTrue(updateProfilePage.getFieldById("lastName").isEnabled());
|
||||
Assert.assertNull(updateProfilePage.getFieldById("postal_code"));
|
||||
updateProfilePage.prepareUpdate().submit();
|
||||
} finally {
|
||||
setLDAPWritable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserProfileWithoutImport() {
|
||||
setLDAPImportDisabled();
|
||||
try {
|
||||
// Test local user is writable and has only attributes defined explicitly in user-profile
|
||||
// Test user profile of user johnkeycloak in admin API
|
||||
UserResource johnResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak2");
|
||||
UserRepresentation john = johnResource.toRepresentation(true);
|
||||
|
||||
assertUser(john, "johnkeycloak2", "john2@email.org", "John", "Doe", "1234");
|
||||
assertProfileAttributes(john, null, false, "username", "email", "firstName", "lastName", "postal_code");
|
||||
assertProfileAttributes(john, USER_METADATA_GROUP, true, LDAPConstants.LDAP_ID, LDAPConstants.LDAP_ENTRY_DN);
|
||||
} finally {
|
||||
setLDAPImportEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private void setLDAPReadOnly() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.toString());
|
||||
appRealm.updateComponent(ctx.getLdapModel());
|
||||
});
|
||||
}
|
||||
|
||||
private void setLDAPWritable() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
|
||||
appRealm.updateComponent(ctx.getLdapModel());
|
||||
});
|
||||
}
|
||||
|
||||
private void setLDAPImportDisabled() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
ctx.getLdapModel().getConfig().putSingle(IMPORT_ENABLED, "false");
|
||||
appRealm.updateComponent(ctx.getLdapModel());
|
||||
});
|
||||
}
|
||||
|
||||
private void setLDAPImportEnabled() {
|
||||
testingClient.server().run(session -> {
|
||||
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||
RealmModel appRealm = ctx.getRealm();
|
||||
|
||||
ctx.getLdapModel().getConfig().putSingle(IMPORT_ENABLED, "true");
|
||||
appRealm.updateComponent(ctx.getLdapModel());
|
||||
});
|
||||
}
|
||||
|
||||
private void assertUser(UserRepresentation user, String expectedUsername, String expectedEmail, String expectedFirstName, String expectedLastname, String expectedPostalCode) {
|
||||
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(expectedPostalCode, user.getAttributes().get("postal_code").get(0));
|
||||
|
||||
Assert.assertNotNull(user.getAttributes().get(LDAPConstants.LDAP_ID));
|
||||
Assert.assertNotNull(user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN));
|
||||
}
|
||||
|
||||
|
||||
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 void assertProfileAttributesNotPresent(UserRepresentation user, String... attributes) {
|
||||
for (String attrName : attributes) {
|
||||
UserProfileAttributeMetadata attrMetadata = user.getUserProfileMetadata().getAttributeMetadata(attrName);
|
||||
Assert.assertNull("Attribute " + attrName + " was present for user " + user.getUsername(), attrMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -42,8 +42,10 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
@ -57,7 +59,14 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
|||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.storage.StorageId;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
|
||||
import org.keycloak.testsuite.federation.ldap.LDAPTestContext;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
import org.keycloak.userprofile.AttributeGroupMetadata;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
|
@ -86,6 +95,9 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
protected static final String ATT_ADDRESS = "address";
|
||||
|
||||
@ClassRule
|
||||
public static LDAPRule ldapRule = new LDAPRule();
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
super.configureTestRealm(testRealm);
|
||||
|
|
Loading…
Reference in a new issue