KEYCLOAK-14536 Migrate UserModel fields to attributes

- In order to make lastName/firstName/email/username field
  configurable in profile
  we need to store it as an attribute
- Keep database as is for now (no impact on performance, schema)
- Keep field names and getters and setters (no impact on FTL files)

Fix tests with logic changes

- PolicyEvaluationTest: We need to take new user attributes into account
- UserTest: We need to take into account new user attributes

Potential impact on users:

- When subclassing UserModel, consistency issues may occur since one can
  now set e.g. username via setSingleAttribute also
- When using PolicyEvaluations, the number of attributes has changed
This commit is contained in:
Martin Idel 2020-05-06 09:58:18 +02:00 committed by Marek Posolda
parent 8a31c331f5
commit 05b6ef8327
27 changed files with 412 additions and 248 deletions

View file

@ -29,7 +29,10 @@ import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -96,7 +99,7 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override
public String getFirstName() {
return firstName != null? firstName : super.getFirstName();
return firstName != null ? firstName : super.getFirstName();
}
@Override
@ -105,19 +108,61 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
}
@Override
public void setFirstName(String firstName) {
this.firstName = firstName;
setFullNameToLDAPObject();
super.setFirstName(firstName);
public List<String> getAttribute(String name) {
if (UserModel.FIRST_NAME.equals(name)) {
return firstName != null ? Collections.singletonList(firstName) : super.getAttribute(name);
} else if (UserModel.LAST_NAME.equals(name)) {
return lastName != null ? Collections.singletonList(lastName) : super.getAttribute(name);
}
return super.getAttribute(name);
}
@Override
public void setLastName(String lastName) {
this.lastName = lastName;
setFullNameToLDAPObject();
super.setLastName(lastName);
public String getFirstAttribute(String name) {
if (UserModel.FIRST_NAME.equals(name)) {
return firstName != null ? firstName : super.getFirstAttribute(name);
} else if (UserModel.LAST_NAME.equals(name)) {
return lastName != null ? lastName : super.getFirstAttribute(name);
}
return super.getFirstAttribute(name);
}
@Override
public void setSingleAttribute(String name, String value) {
if (UserModel.FIRST_NAME.equals(name)) {
this.firstName = value;
setFullNameToLDAPObject();
} else if (UserModel.LAST_NAME.equals(name)) {
this.lastName = value;
setFullNameToLDAPObject();
}
super.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
if (UserModel.FIRST_NAME.equals(name)) {
this.firstName = values.get(0);
setFullNameToLDAPObject();
} else if (UserModel.LAST_NAME.equals(name)) {
this.lastName = values.get(0);
setFullNameToLDAPObject();
}
super.setSingleAttribute(name, values.get(0));
}
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> attributes = delegate.getAttributes();
if (firstName != null) {
attributes.put(UserModel.FIRST_NAME, Collections.singletonList(firstName));
} else if (lastName != null) {
attributes.put(UserModel.FIRST_NAME, Collections.singletonList(lastName));
}
return attributes;
}
private void setFullNameToLDAPObject() {
String fullName = getFullNameForWriteToLDAP(getFirstName(), getLastName(), getUsername());
if (logger.isTraceEnabled()) {
@ -130,7 +175,6 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
String ldapFullNameAttrName = getLdapFullNameAttrName();
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
}
};
return txDelegate;
@ -183,7 +227,7 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
return;
}
EscapeStrategy escapeStrategy = firstNameCondition!=null ? firstNameCondition.getEscapeStrategy() : lastNameCondition.getEscapeStrategy();
EscapeStrategy escapeStrategy = firstNameCondition != null ? firstNameCondition.getEscapeStrategy() : lastNameCondition.getEscapeStrategy();
EqualCondition fullNameCondition = new EqualCondition(ldapFullNameAttrName, fullName, escapeStrategy);
query.addWhereCondition(fullNameCondition);

View file

@ -191,6 +191,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override
public void setSingleAttribute(String name, String value) {
if (UserModel.USERNAME.equals(name)) {
checkDuplicateUsername(userModelAttrName, value, realm, ldapProvider.getSession(), this);
} else if (UserModel.EMAIL.equals(name)) {
checkDuplicateEmail(userModelAttrName, value, realm, ldapProvider.getSession(), this);
}
if (setLDAPAttribute(name, value)) {
super.setSingleAttribute(name, value);
}
@ -198,6 +203,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override
public void setAttribute(String name, List<String> values) {
if (UserModel.USERNAME.equals(name)) {
checkDuplicateUsername(userModelAttrName, values.get(0), realm, ldapProvider.getSession(), this);
} else if (UserModel.EMAIL.equals(name)) {
checkDuplicateEmail(userModelAttrName, values.get(0), realm, ldapProvider.getSession(), this);
}
if (setLDAPAttribute(name, values)) {
super.setAttribute(name, values);
}

View file

@ -21,6 +21,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ReadOnlyException;
import java.util.List;
/**
* Readonly proxy for a SSSD UserModel that prevents attributes from being updated.
*
@ -52,6 +54,24 @@ public class ReadonlySSSDUserModelDelegate extends UserModelDelegate implements
throw new ReadOnlyException("Federated storage is not writable");
}
@Override
public void setSingleAttribute(String name, String value) {
setSpecialAttributesToReadonly(name);
super.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> value) {
setSpecialAttributesToReadonly(name);
super.setAttribute(name, value);
}
private void setSpecialAttributesToReadonly(String name) {
if (UserModel.FIRST_NAME.equals(name) || UserModel.LAST_NAME.equals(name) || UserModel.EMAIL.equals(name) || USERNAME.equals(name)) {
throw new ReadOnlyException("Federated storage is not writable");
}
}
@Override
public void setEmail(String email) {
throw new ReadOnlyException("Federated storage is not writable");

View file

@ -59,6 +59,37 @@ public class UserAdapter implements CachedUserModel {
this.modelSupplier = this::getUserModel;
}
@Override
public String getFirstName() {
return getFirstAttribute(FIRST_NAME);
}
@Override
public void setFirstName(String firstName) {
setSingleAttribute(FIRST_NAME, firstName);
}
@Override
public String getLastName() {
return getFirstAttribute(LAST_NAME);
}
@Override
public void setLastName(String lastName) {
setSingleAttribute(LAST_NAME, lastName);
}
@Override
public String getEmail() {
return getFirstAttribute(EMAIL);
}
@Override
public void setEmail(String email) {
email = email == null ? null : email.toLowerCase();
setSingleAttribute(EMAIL, email);
}
@Override
public UserModel getDelegateForUpdate() {
if (updated == null) {
@ -101,15 +132,13 @@ public class UserAdapter implements CachedUserModel {
@Override
public String getUsername() {
if (updated != null) return updated.getUsername();
return cached.getUsername();
return getFirstAttribute(UserModel.USERNAME);
}
@Override
public void setUsername(String username) {
getDelegateForUpdate();
username = KeycloakModelUtils.toLowerCaseSafe(username);
updated.setUsername(username);
username = username==null ? null : username.toLowerCase();
setSingleAttribute(UserModel.USERNAME, username);
}
@Override
@ -202,43 +231,6 @@ public class UserAdapter implements CachedUserModel {
updated.removeRequiredAction(action);
}
@Override
public String getFirstName() {
if (updated != null) return updated.getFirstName();
return cached.getFirstName();
}
@Override
public void setFirstName(String firstName) {
getDelegateForUpdate();
updated.setFirstName(firstName);
}
@Override
public String getLastName() {
if (updated != null) return updated.getLastName();
return cached.getLastName();
}
@Override
public void setLastName(String lastName) {
getDelegateForUpdate();
updated.setLastName(lastName);
}
@Override
public String getEmail() {
if (updated != null) return updated.getEmail();
return cached.getEmail();
}
@Override
public void setEmail(String email) {
getDelegateForUpdate();
email = KeycloakModelUtils.toLowerCaseSafe(email);
updated.setEmail(email);
}
@Override
public boolean isEmailVerified() {
if (updated != null) return updated.isEmailVerified();

View file

@ -40,8 +40,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
private final String realm;
private final String username;
private final Long createdTimestamp;
private final String firstName;
private final String lastName;
private final String email;
private final boolean emailVerified;
private final boolean enabled;
@ -58,8 +56,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
this.realm = realm.getId();
this.username = user.getUsername();
this.createdTimestamp = user.getCreatedTimestamp();
this.firstName = user.getFirstName();
this.lastName = user.getLastName();
this.email = user.getEmail();
this.emailVerified = user.isEmailVerified();
this.enabled = user.isEnabled();
@ -84,14 +80,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
return createdTimestamp;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() {
return email;
}

View file

@ -50,6 +50,7 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.client.ClientStorageProvider;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
@ -852,22 +853,22 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
case UserModel.SEARCH:
List<Predicate> orPredicates = new ArrayList();
orPredicates.add(builder.like(builder.lower(root.get(UserModel.USERNAME)), "%" + value.toLowerCase() + "%"));
orPredicates.add(builder.like(builder.lower(root.get(UserModel.EMAIL)), "%" + value.toLowerCase() + "%"));
orPredicates.add(builder.like(builder.lower(root.get(USERNAME)), "%" + value.toLowerCase() + "%"));
orPredicates.add(builder.like(builder.lower(root.get(EMAIL)), "%" + value.toLowerCase() + "%"));
orPredicates.add(builder.like(
builder.lower(builder.concat(builder.concat(
builder.coalesce(root.get(UserModel.FIRST_NAME), builder.literal("")), " "),
builder.coalesce(root.get(UserModel.LAST_NAME), builder.literal("")))),
builder.coalesce(root.get(FIRST_NAME), builder.literal("")), " "),
builder.coalesce(root.get(LAST_NAME), builder.literal("")))),
"%" + value.toLowerCase() + "%"));
predicates.add(builder.or(orPredicates.toArray(new Predicate[orPredicates.size()])));
break;
case UserModel.USERNAME:
case UserModel.FIRST_NAME:
case UserModel.LAST_NAME:
case UserModel.EMAIL:
case USERNAME:
case FIRST_NAME:
case LAST_NAME:
case EMAIL:
if (Boolean.valueOf(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))) {
predicates.add(builder.equal(builder.lower(root.get(key)), value.toLowerCase()));
} else {

View file

@ -44,6 +44,7 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
@ -115,6 +116,20 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public void setSingleAttribute(String name, String value) {
if (UserModel.FIRST_NAME.equals(name)) {
user.setFirstName(value);
return;
} else if (UserModel.LAST_NAME.equals(name)) {
user.setLastName(value);
return;
} else if (UserModel.EMAIL.equals(name)) {
setEmail(value);
return;
} else if (UserModel.USERNAME.equals(name)) {
setUsername(value);
return;
}
// Remove all existing
if (value == null) {
user.getAttributes().removeIf(a -> a.getName().equals(name));
} else {
@ -149,6 +164,19 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public void setAttribute(String name, List<String> values) {
if (UserModel.FIRST_NAME.equals(name)) {
user.setFirstName(values.get(0));
return;
} else if (UserModel.LAST_NAME.equals(name)) {
user.setLastName(values.get(0));
return;
} else if (UserModel.EMAIL.equals(name)) {
setEmail(values.get(0));
return;
} else if (UserModel.USERNAME.equals(name)) {
setUsername(values.get(0));
return;
}
// Remove all existing
removeAttribute(name);
for (Iterator<String> it = values.stream().filter(Objects::nonNull).iterator(); it.hasNext();) {
@ -190,6 +218,15 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public String getFirstAttribute(String name) {
if (UserModel.FIRST_NAME.equals(name)) {
return user.getFirstName();
} else if (UserModel.LAST_NAME.equals(name)) {
return user.getLastName();
} else if (UserModel.EMAIL.equals(name)) {
return user.getEmail();
} else if (UserModel.USERNAME.equals(name)) {
return user.getUsername();
}
for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) {
return attr.getValue();
@ -200,6 +237,15 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public List<String> getAttribute(String name) {
if (UserModel.FIRST_NAME.equals(name)) {
return Collections.singletonList(user.getFirstName());
} else if (UserModel.LAST_NAME.equals(name)) {
return Collections.singletonList(user.getLastName());
} else if (UserModel.EMAIL.equals(name)) {
return Collections.singletonList(user.getEmail());
} else if (UserModel.USERNAME.equals(name)) {
return Collections.singletonList(user.getUsername());
}
List<String> result = new ArrayList<>();
for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) {
@ -215,6 +261,10 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
for (UserAttributeEntity attr : user.getAttributes()) {
result.add(attr.getName(), attr.getValue());
}
result.add(UserModel.FIRST_NAME, user.getFirstName());
result.add(UserModel.LAST_NAME, user.getLastName());
result.add(UserModel.EMAIL, user.getEmail());
result.add(UserModel.USERNAME, user.getUsername());
return result;
}

View file

@ -223,8 +223,18 @@ public class ModelToRepresentation {
rep.setRequiredActions(reqActions);
if (user.getAttributes() != null && !user.getAttributes().isEmpty()) {
Map<String, List<String>> attrs = new HashMap<>(user.getAttributes());
Map<String, List<String>> attributes = user.getAttributes();
Map<String, List<String>> copy = null;
if (attributes != null) {
copy = new HashMap<>(attributes);
copy.remove(UserModel.LAST_NAME);
copy.remove(UserModel.FIRST_NAME);
copy.remove(UserModel.EMAIL);
copy.remove(UserModel.USERNAME);
}
if (attributes != null && !copy.isEmpty()) {
Map<String, List<String>> attrs = new HashMap<>(copy);
rep.setAttributes(attrs);
}

View file

@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.ReadOnlyException;
@ -39,17 +40,11 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class InMemoryUserAdapter implements UserModel {
private String username;
public class InMemoryUserAdapter extends UserModelDefaultMethods {
private Long createdTimestamp = Time.currentTimeMillis();
private String firstName;
private String lastName;
private String email;
private boolean emailVerified;
private boolean enabled;
private String realmId;
private Set<String> roleIds = new HashSet<>();
private Set<String> groupIds = new HashSet<>();
@ -67,8 +62,17 @@ public class InMemoryUserAdapter implements UserModel {
this.session = session;
this.realm = realm;
this.id = id;
}
@Override
public String getUsername() {
return getFirstAttribute(UserModel.USERNAME);
}
@Override
public void setUsername(String username) {
username = username==null ? null : username.toLowerCase();
setSingleAttribute(UserModel.USERNAME, username);
}
public void addDefaults() {
@ -93,18 +97,6 @@ public class InMemoryUserAdapter implements UserModel {
return id;
}
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
checkReadonly();
this.username = username.toLowerCase();
}
@Override
public Long getCreatedTimestamp() {
return createdTimestamp;
@ -200,43 +192,6 @@ public class InMemoryUserAdapter implements UserModel {
requiredActions.remove(action.name());
}
@Override
public String getFirstName() {
return firstName;
}
@Override
public void setFirstName(String firstName) {
checkReadonly();
this.firstName = firstName;
}
@Override
public String getLastName() {
return lastName;
}
@Override
public void setLastName(String lastName) {
checkReadonly();
this.lastName = lastName;
}
@Override
public String getEmail() {
return email;
}
@Override
public void setEmail(String email) {
checkReadonly();
if (email != null) email = email.toLowerCase();
this.email = email;
}
@Override
public boolean isEmailVerified() {
return emailVerified;

View file

@ -31,8 +31,8 @@ import java.util.stream.Collectors;
*/
public interface UserModel extends RoleMapperModel {
String USERNAME = "username";
String LAST_NAME = "lastName";
String FIRST_NAME = "firstName";
String LAST_NAME = "lastName";
String EMAIL = "email";
String LOCALE = "locale";
String INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account";
@ -48,10 +48,12 @@ public interface UserModel extends RoleMapperModel {
String getId();
// No default method here to allow Abstract subclasses where the username is provided in a different manner
String getUsername();
// No default method here to allow Abstract subclasses where the username is provided in a different manner
void setUsername(String username);
/**
* Get timestamp of user creation. May be null for old users created before this feature introduction.
*/

View file

@ -0,0 +1,56 @@
/*
* Copyright 2020 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.models;
/**
* @author <a href="mailto:external.Martin.Idel@bosch.io">Martin Idel</a>
* @version $Revision: 1 $
*/
public abstract class UserModelDefaultMethods implements UserModel {
@Override
public String getFirstName() {
return getFirstAttribute(FIRST_NAME);
}
@Override
public void setFirstName(String firstName) {
setSingleAttribute(FIRST_NAME, firstName);
}
@Override
public String getLastName() {
return getFirstAttribute(LAST_NAME);
}
@Override
public void setLastName(String lastName) {
setSingleAttribute(LAST_NAME, lastName);
}
@Override
public String getEmail() {
return getFirstAttribute(EMAIL);
}
@Override
public void setEmail(String email) {
email = email == null ? null : email.toLowerCase();
setSingleAttribute(EMAIL, email);
}
}

View file

@ -25,6 +25,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.ReadOnlyException;
@ -49,7 +50,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractUserAdapter implements UserModel {
public abstract class AbstractUserAdapter extends UserModelDefaultMethods {
protected KeycloakSession session;
protected RealmModel realm;
protected ComponentModel storageProviderModel;
@ -313,16 +314,24 @@ public abstract class AbstractUserAdapter implements UserModel {
@Override
public String getFirstAttribute(String name) {
if (name.equals(UserModel.USERNAME)) {
return getUsername();
}
return null;
}
@Override
public Map<String, List<String>> getAttributes() {
return new MultivaluedHashMap<>();
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
attributes.add(UserModel.USERNAME, getUsername());
return attributes;
}
@Override
public List<String> getAttribute(String name) {
if (name.equals(UserModel.USERNAME)) {
return Collections.singletonList(getUsername());
}
return Collections.emptyList();
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.storage.adapter;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
@ -24,6 +25,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.StorageId;
@ -45,7 +47,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
public abstract class AbstractUserAdapterFederatedStorage extends UserModelDefaultMethods {
public static String FIRST_NAME_ATTRIBUTE = "FIRST_NAME";
public static String LAST_NAME_ATTRIBUTE = "LAST_NAME";
public static String EMAIL_ATTRIBUTE = "EMAIL";
@ -336,7 +338,10 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
@Override
public void setSingleAttribute(String name, String value) {
getFederatedStorage().setSingleAttribute(realm, this.getId(), name, value);
if (UserModel.USERNAME.equals(name)) {
setUsername(value);
}
getFederatedStorage().setSingleAttribute(realm, this.getId(), mapAttribute(name), value);
}
@ -348,75 +353,55 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
@Override
public void setAttribute(String name, List<String> values) {
getFederatedStorage().setAttribute(realm, this.getId(), name, values);
if (UserModel.USERNAME.equals(name)) {
setUsername(values.get(0));
}
getFederatedStorage().setAttribute(realm, this.getId(), mapAttribute(name), values);
}
@Override
public String getFirstAttribute(String name) {
return getFederatedStorage().getAttributes(realm, this.getId()).getFirst(name);
if (UserModel.USERNAME.equals(name)) {
return getUsername();
}
return getFederatedStorage().getAttributes(realm, this.getId()).getFirst(mapAttribute(name));
}
@Override
public Map<String, List<String>> getAttributes() {
return getFederatedStorage().getAttributes(realm, this.getId());
MultivaluedHashMap<String, String> attributes = getFederatedStorage().getAttributes(realm, this.getId());
if (attributes == null) {
attributes = new MultivaluedHashMap<>();
}
List<String> firstName = attributes.remove(FIRST_NAME_ATTRIBUTE);
attributes.add(UserModel.FIRST_NAME, firstName != null && firstName.size() >= 1 ? firstName.get(0) : null);
List<String> lastName = attributes.remove(LAST_NAME_ATTRIBUTE);
attributes.add(UserModel.LAST_NAME, lastName != null && lastName.size() >= 1 ? lastName.get(0) : null);
List<String> email = attributes.remove(EMAIL_ATTRIBUTE);
attributes.add(UserModel.EMAIL, email != null && email.size() >= 1 ? email.get(0) : null);
attributes.add(UserModel.USERNAME, getUsername());
return attributes;
}
@Override
public List<String> getAttribute(String name) {
List<String> result = getFederatedStorage().getAttributes(realm, this.getId()).get(name);
if (UserModel.USERNAME.equals(name)) {
return Collections.singletonList(getUsername());
}
List<String> result = getFederatedStorage().getAttributes(realm, this.getId()).get(mapAttribute(name));
return (result == null) ? Collections.emptyList() : result;
}
@Override
public String getFirstName() {
return getFirstAttribute(FIRST_NAME_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* FIRST_NAME_ATTRIBUTE
*
* @param firstName
*/
@Override
public void setFirstName(String firstName) {
setSingleAttribute(FIRST_NAME_ATTRIBUTE, firstName);
}
@Override
public String getLastName() {
return getFirstAttribute(LAST_NAME_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* LAST_NAME_ATTRIBUTE
*
* @param lastName
*/
@Override
public void setLastName(String lastName) {
setSingleAttribute(LAST_NAME_ATTRIBUTE, lastName);
}
@Override
public String getEmail() {
return getFirstAttribute(EMAIL_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* EMAIL_ATTRIBUTE
*
* @param email
*/
@Override
public void setEmail(String email) {
setSingleAttribute(EMAIL_ATTRIBUTE, email);
private String mapAttribute(String attributeName) {
if (UserModel.FIRST_NAME.equals(attributeName)) {
return FIRST_NAME_ATTRIBUTE;
} else if (UserModel.LAST_NAME.equals(attributeName)) {
return LAST_NAME_ATTRIBUTE;
} else if (UserModel.EMAIL.equals(attributeName)) {
return EMAIL_ATTRIBUTE;
}
return attributeName;
}
@Override

View file

@ -29,6 +29,8 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.openshift.OpenShiftTokenReviewResponseRepresentation;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
@ -46,10 +48,6 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
private String id;
private String brokerUsername;
private String modelUsername;
private String email;
private String firstName;
private String lastName;
private String brokerSessionId;
private String brokerUserId;
private String code;
@ -78,20 +76,20 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
@JsonIgnore
@Override
public String getUsername() {
return modelUsername;
return getFirstAttribute(UserModel.USERNAME);
}
@Override
public void setUsername(String username) {
this.modelUsername = username;
setSingleAttribute(UserModel.USERNAME, username);
}
public String getModelUsername() {
return modelUsername;
return getFirstAttribute(UserModel.USERNAME);
}
public void setModelUsername(String modelUsername) {
this.modelUsername = modelUsername;
setSingleAttribute(UserModel.USERNAME, modelUsername);
}
public String getBrokerUsername() {
@ -104,32 +102,32 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
@Override
public String getEmail() {
return email;
return getFirstAttribute(UserModel.EMAIL);
}
@Override
public void setEmail(String email) {
this.email = email;
setSingleAttribute(UserModel.EMAIL, email);
}
@Override
public String getFirstName() {
return firstName;
return getFirstAttribute(UserModel.FIRST_NAME);
}
@Override
public void setFirstName(String firstName) {
this.firstName = firstName;
setSingleAttribute(UserModel.FIRST_NAME, firstName);
}
@Override
public String getLastName() {
return lastName;
return getFirstAttribute(UserModel.LAST_NAME);
}
@Override
public void setLastName(String lastName) {
this.lastName = lastName;
setSingleAttribute(UserModel.LAST_NAME, lastName);
}
public String getBrokerSessionId() {

View file

@ -143,7 +143,14 @@ public class AccountRestService {
rep.setLastName(user.getLastName());
rep.setEmail(user.getEmail());
rep.setEmailVerified(user.isEmailVerified());
rep.setAttributes(user.getAttributes());
rep.setEmailVerified(user.isEmailVerified());
Map<String, List<String>> attributes = user.getAttributes();
Map<String, List<String>> copiedAttributes = new HashMap<>(attributes);
copiedAttributes.remove(UserModel.FIRST_NAME);
copiedAttributes.remove(UserModel.LAST_NAME);
copiedAttributes.remove(UserModel.EMAIL);
copiedAttributes.remove(UserModel.USERNAME);
rep.setAttributes(copiedAttributes);
return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build();
}

View file

@ -160,6 +160,10 @@ public class UserResource {
if (rep.getAttributes() != null) {
attrsToRemove = new HashSet<>(user.getAttributes().keySet());
attrsToRemove.removeAll(rep.getAttributes().keySet());
attrsToRemove.remove(UserModel.FIRST_NAME);
attrsToRemove.remove(UserModel.LAST_NAME);
attrsToRemove.remove(UserModel.EMAIL);
attrsToRemove.remove(UserModel.USERNAME);
} else {
attrsToRemove = Collections.emptySet();
}

View file

@ -74,7 +74,7 @@ public class FreeMarkerUtil {
private Template getTemplate(String templateName, Theme theme) throws IOException {
Configuration cfg = new Configuration();
// Assume *.ftl files are html. This lets freemarker know how to
// sanitize and prevent XSS attacks.
if (templateName.toLowerCase().endsWith(".ftl")) {

View file

@ -128,7 +128,7 @@ public class FailableHardcodedStorageProvider implements UserStorageProvider, Us
@Override
public void setUsername(String name) {
super.setUsername(name);
name = name;
username = name;
}
@Override

View file

@ -76,6 +76,11 @@ public class LDAPTestUtils {
return username;
}
@Override
public String getEmail() {
return email;
}
@Override
public String getFirstName() {
return firstName;
@ -87,13 +92,30 @@ public class LDAPTestUtils {
}
@Override
public String getEmail() {
return email;
public String getFirstAttribute(String name) {
if (UserModel.LAST_NAME.equals(name)) {
return lastName;
} else if (UserModel.FIRST_NAME.equals(name)) {
return firstName;
} else if (UserModel.EMAIL.equals(name)) {
return email;
} else if (UserModel.USERNAME.equals(name)) {
return username;
}
return super.getFirstAttribute(name);
}
@Override
public List<String> getAttribute(String name) {
if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
if (UserModel.LAST_NAME.equals(name)) {
return Collections.singletonList(lastName);
} else if (UserModel.FIRST_NAME.equals(name)) {
return Collections.singletonList(firstName);
} else if (UserModel.EMAIL.equals(name)) {
return Collections.singletonList(email);
} else if (UserModel.USERNAME.equals(name)) {
return Collections.singletonList(username);
} else if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
return Arrays.asList(postalCode);
} else if ("street".equals(name) && street != null) {
return Collections.singletonList(street);

View file

@ -580,7 +580,7 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
builder.append("var realm = $evaluation.getRealm();");
builder.append("var attributes = realm.getUserAttributes('jdoe');");
builder.append("if (attributes.size() == 2 && attributes.containsKey('a1') && attributes.containsKey('a2') && attributes.get('a1').size() == 2 && attributes.get('a2').get(0).equals('3')) { $evaluation.grant(); }");
builder.append("if (attributes.size() == 6 && attributes.containsKey('a1') && attributes.containsKey('a2') && attributes.get('a1').size() == 2 && attributes.get('a2').get(0).equals('3')) { $evaluation.grant(); }");
policyRepresentation.setCode(builder.toString());

View file

@ -204,7 +204,7 @@ public class ExportImportUtil {
// Test attributes
Map<String, List<String>> attrs = wburke.getAttributes();
Assert.assertEquals(1, attrs.size());
List<String> attrVals = attrs.get("email");
List<String> attrVals = attrs.get("old-email");
Assert.assertEquals(1, attrVals.size());
Assert.assertEquals("bburke@redhat.com", attrVals.get(0));

View file

@ -197,6 +197,11 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest {
return username;
}
@Override
public String getEmail() {
return email;
}
@Override
public String getFirstName() {
return firstName;
@ -208,13 +213,30 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest {
}
@Override
public String getEmail() {
return email;
public String getFirstAttribute(String name) {
if (UserModel.LAST_NAME.equals(name)) {
return lastName;
} else if (UserModel.FIRST_NAME.equals(name)) {
return firstName;
} else if (UserModel.EMAIL.equals(name)) {
return email;
} else if (UserModel.USERNAME.equals(name)) {
return username;
}
return super.getFirstAttribute(name);
}
@Override
public List<String> getAttribute(String name) {
if (LDAPConstants.JPEG_PHOTO.equals(name)) {
if (UserModel.LAST_NAME.equals(name)) {
return Collections.singletonList(lastName);
} else if (UserModel.FIRST_NAME.equals(name)) {
return Collections.singletonList(firstName);
} else if (UserModel.EMAIL.equals(name)) {
return Collections.singletonList(email);
} else if (UserModel.USERNAME.equals(name)) {
return Collections.singletonList(username);
} else if (LDAPConstants.JPEG_PHOTO.equals(name)) {
return Arrays.asList(jpegPhoto);
} else {
return Collections.emptyList();

View file

@ -95,14 +95,6 @@ public class LDAPLegacyImportTest extends AbstractLDAPTest {
});
}
//@Test
public void runit() throws Exception {
Thread.sleep(10000000);
}
@Test
public void loginClassic() {
loginPage.open();

View file

@ -114,15 +114,6 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
});
}
// @Test
// @Ignore
// public void runit() throws Exception {
// Thread.sleep(10000000);
//
// }
/**
* KEYCLOAK-3986
*

View file

@ -36,6 +36,7 @@ import org.keycloak.testsuite.util.RealmBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -237,7 +238,7 @@ public class UserModelTest extends AbstractTestRealmKeycloakTest {
List<String> attrVals = user.getAttribute("key1");
Assert.assertThat(attrVals, hasSize(1));
Assert.assertThat(attrVals.get(0), equalTo("value1"));
Assert.assertThat(attrVals, contains("value1"));
Assert.assertThat(user.getFirstAttribute("key1"), equalTo("value1"));
attrVals = user.getAttribute("key2");
@ -249,7 +250,8 @@ public class UserModelTest extends AbstractTestRealmKeycloakTest {
Assert.assertThat(user.getFirstAttribute("key3"), nullValue());
Map<String, List<String>> allAttrVals = user.getAttributes();
Assert.assertThat(allAttrVals.keySet(), hasSize(2));
Assert.assertThat(allAttrVals.keySet(), hasSize(6));
Assert.assertThat(allAttrVals.keySet(), containsInAnyOrder(UserModel.USERNAME, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL, "key1", "key2"));
Assert.assertThat(allAttrVals.get("key1"), equalTo(user.getAttribute("key1")));
Assert.assertThat(allAttrVals.get("key2"), equalTo(user.getAttribute("key2")));
@ -298,9 +300,9 @@ public class UserModelTest extends AbstractTestRealmKeycloakTest {
Map<String, List<String>> allAttrVals = user.getAttributes();
// Ensure same transaction is able to see updated value
Assert.assertThat(allAttrVals.keySet(), hasSize(1));
Assert.assertThat(allAttrVals.keySet(), hasSize(5));
Assert.assertThat(allAttrVals.keySet(), containsInAnyOrder("key1", UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL, UserModel.USERNAME));
Assert.assertThat(allAttrVals.get("key1"), contains("val2"));
});
}
@ -316,8 +318,12 @@ public class UserModelTest extends AbstractTestRealmKeycloakTest {
RealmModel realm = currentSession.realms().getRealmByName("original");
Map<String, List<String>> expected = new HashMap<>();
expected.put("key1", Arrays.asList("value3"));
expected.put("key2", Arrays.asList("value2"));
expected.put("key1", Collections.singletonList("value3"));
expected.put("key2", Collections.singletonList("value2"));
expected.put(UserModel.FIRST_NAME, Collections.singletonList(null));
expected.put(UserModel.LAST_NAME, Collections.singletonList(null));
expected.put(UserModel.EMAIL, Collections.singletonList(null));
expected.put(UserModel.USERNAME, Collections.singletonList("user"));
UserModel user = currentSession.users().addUser(realm, "user");

View file

@ -129,7 +129,7 @@
"createdTimestamp" : 123654,
"notBefore": 159,
"attributes": {
"email": "bburke@redhat.com"
"old-email": "bburke@redhat.com"
},
"credentials": [
{

View file

@ -148,14 +148,6 @@ public class LdapManyObjectsInitializerCommand extends AbstractCommand {
private static LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
final String firstName, final String lastName, final String email,
String groupsDN, int startOffsetGroups, int countGroups) {
// LDAPObject ldapUser = new LDAPObject();
// LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
// ldapUser.setRdnAttributeName(ldapConfig.getRdnLdapAttribute());
// ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
// LDAPUtils.computeAndSetDn(ldapConfig, ldapUser);
//
// ldapUser.setSingleAttribute("uid", )
// ldapProvider.getLdapIdentityStore().add(ldapUser);
UserModel helperUser = new UserModelDelegate(null) {
@ -181,7 +173,15 @@ public class LdapManyObjectsInitializerCommand extends AbstractCommand {
@Override
public List<String> getAttribute(String name) {
if ("street".equals(name)) {
if (UserModel.FIRST_NAME.equals(name)) {
return Collections.singletonList(firstName);
} else if (UserModel.LAST_NAME.equals(name)) {
return Collections.singletonList(lastName);
} else if (UserModel.EMAIL.equals(name)) {
return Collections.singletonList(email);
} else if (UserModel.USERNAME.equals(name)) {
return Collections.singletonList(username);
} else if ("street".equals(name)) {
List<String> groupNamesList = new ArrayList<>();
for (int i=startOffsetGroups ; i<startOffsetGroups + countGroups ; i++) {