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.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery; import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -105,18 +108,60 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
} }
@Override @Override
public void setFirstName(String firstName) { public List<String> getAttribute(String name) {
this.firstName = firstName; if (UserModel.FIRST_NAME.equals(name)) {
setFullNameToLDAPObject(); return firstName != null ? Collections.singletonList(firstName) : super.getAttribute(name);
super.setFirstName(firstName); } else if (UserModel.LAST_NAME.equals(name)) {
return lastName != null ? Collections.singletonList(lastName) : super.getAttribute(name);
}
return super.getAttribute(name);
} }
@Override @Override
public void setLastName(String lastName) { public String getFirstAttribute(String name) {
this.lastName = lastName; if (UserModel.FIRST_NAME.equals(name)) {
setFullNameToLDAPObject(); return firstName != null ? firstName : super.getFirstAttribute(name);
super.setLastName(lastName); } 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() { private void setFullNameToLDAPObject() {
String fullName = getFullNameForWriteToLDAP(getFirstName(), getLastName(), getUsername()); String fullName = getFullNameForWriteToLDAP(getFirstName(), getLastName(), getUsername());
@ -130,7 +175,6 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
String ldapFullNameAttrName = getLdapFullNameAttrName(); String ldapFullNameAttrName = getLdapFullNameAttrName();
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName); ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
} }
}; };
return txDelegate; return txDelegate;

View file

@ -191,6 +191,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override @Override
public void setSingleAttribute(String name, String value) { 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)) { if (setLDAPAttribute(name, value)) {
super.setSingleAttribute(name, value); super.setSingleAttribute(name, value);
} }
@ -198,6 +203,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override @Override
public void setAttribute(String name, List<String> values) { 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)) { if (setLDAPAttribute(name, values)) {
super.setAttribute(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.models.utils.UserModelDelegate;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import java.util.List;
/** /**
* Readonly proxy for a SSSD UserModel that prevents attributes from being updated. * 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"); 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 @Override
public void setEmail(String email) { public void setEmail(String email) {
throw new ReadOnlyException("Federated storage is not writable"); throw new ReadOnlyException("Federated storage is not writable");

View file

@ -59,6 +59,37 @@ public class UserAdapter implements CachedUserModel {
this.modelSupplier = this::getUserModel; 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 @Override
public UserModel getDelegateForUpdate() { public UserModel getDelegateForUpdate() {
if (updated == null) { if (updated == null) {
@ -101,15 +132,13 @@ public class UserAdapter implements CachedUserModel {
@Override @Override
public String getUsername() { public String getUsername() {
if (updated != null) return updated.getUsername(); return getFirstAttribute(UserModel.USERNAME);
return cached.getUsername();
} }
@Override @Override
public void setUsername(String username) { public void setUsername(String username) {
getDelegateForUpdate(); username = username==null ? null : username.toLowerCase();
username = KeycloakModelUtils.toLowerCaseSafe(username); setSingleAttribute(UserModel.USERNAME, username);
updated.setUsername(username);
} }
@Override @Override
@ -202,43 +231,6 @@ public class UserAdapter implements CachedUserModel {
updated.removeRequiredAction(action); 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 @Override
public boolean isEmailVerified() { public boolean isEmailVerified() {
if (updated != null) return updated.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 realm;
private final String username; private final String username;
private final Long createdTimestamp; private final Long createdTimestamp;
private final String firstName;
private final String lastName;
private final String email; private final String email;
private final boolean emailVerified; private final boolean emailVerified;
private final boolean enabled; private final boolean enabled;
@ -58,8 +56,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
this.realm = realm.getId(); this.realm = realm.getId();
this.username = user.getUsername(); this.username = user.getUsername();
this.createdTimestamp = user.getCreatedTimestamp(); this.createdTimestamp = user.getCreatedTimestamp();
this.firstName = user.getFirstName();
this.lastName = user.getLastName();
this.email = user.getEmail(); this.email = user.getEmail();
this.emailVerified = user.isEmailVerified(); this.emailVerified = user.isEmailVerified();
this.enabled = user.isEnabled(); this.enabled = user.isEnabled();
@ -84,14 +80,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
return createdTimestamp; return createdTimestamp;
} }
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getEmail() { public String getEmail() {
return email; return email;
} }

View file

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

View file

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

View file

@ -223,8 +223,18 @@ public class ModelToRepresentation {
rep.setRequiredActions(reqActions); rep.setRequiredActions(reqActions);
if (user.getAttributes() != null && !user.getAttributes().isEmpty()) { Map<String, List<String>> attributes = user.getAttributes();
Map<String, List<String>> attrs = new HashMap<>(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); rep.setAttributes(attrs);
} }

View file

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

View file

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

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

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.storage.adapter; package org.keycloak.storage.adapter;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
@ -24,6 +25,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.DefaultRoles; import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.RoleUtils; import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
@ -45,7 +47,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @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 FIRST_NAME_ATTRIBUTE = "FIRST_NAME";
public static String LAST_NAME_ATTRIBUTE = "LAST_NAME"; public static String LAST_NAME_ATTRIBUTE = "LAST_NAME";
public static String EMAIL_ATTRIBUTE = "EMAIL"; public static String EMAIL_ATTRIBUTE = "EMAIL";
@ -336,7 +338,10 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
@Override @Override
public void setSingleAttribute(String name, String value) { 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 @Override
public void setAttribute(String name, List<String> values) { 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 @Override
public String getFirstAttribute(String name) { 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 @Override
public Map<String, List<String>> getAttributes() { 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 @Override
public List<String> getAttribute(String name) { 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; return (result == null) ? Collections.emptyList() : result;
} }
@Override private String mapAttribute(String attributeName) {
public String getFirstName() { if (UserModel.FIRST_NAME.equals(attributeName)) {
return getFirstAttribute(FIRST_NAME_ATTRIBUTE); 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;
/**
* 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);
} }
@Override @Override

View file

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

View file

@ -143,7 +143,14 @@ public class AccountRestService {
rep.setLastName(user.getLastName()); rep.setLastName(user.getLastName());
rep.setEmail(user.getEmail()); rep.setEmail(user.getEmail());
rep.setEmailVerified(user.isEmailVerified()); 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(); 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) { if (rep.getAttributes() != null) {
attrsToRemove = new HashSet<>(user.getAttributes().keySet()); attrsToRemove = new HashSet<>(user.getAttributes().keySet());
attrsToRemove.removeAll(rep.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 { } else {
attrsToRemove = Collections.emptySet(); attrsToRemove = Collections.emptySet();
} }

View file

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

View file

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

View file

@ -580,7 +580,7 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
builder.append("var realm = $evaluation.getRealm();"); builder.append("var realm = $evaluation.getRealm();");
builder.append("var attributes = realm.getUserAttributes('jdoe');"); 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()); policyRepresentation.setCode(builder.toString());

View file

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

View file

@ -197,6 +197,11 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest {
return username; return username;
} }
@Override
public String getEmail() {
return email;
}
@Override @Override
public String getFirstName() { public String getFirstName() {
return firstName; return firstName;
@ -208,13 +213,30 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest {
} }
@Override @Override
public String getEmail() { 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; return email;
} else if (UserModel.USERNAME.equals(name)) {
return username;
}
return super.getFirstAttribute(name);
} }
@Override @Override
public List<String> getAttribute(String name) { 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); return Arrays.asList(jpegPhoto);
} else { } else {
return Collections.emptyList(); return Collections.emptyList();

View file

@ -95,14 +95,6 @@ public class LDAPLegacyImportTest extends AbstractLDAPTest {
}); });
} }
//@Test
public void runit() throws Exception {
Thread.sleep(10000000);
}
@Test @Test
public void loginClassic() { public void loginClassic() {
loginPage.open(); 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 * KEYCLOAK-3986
* *

View file

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

View file

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

View file

@ -148,14 +148,6 @@ public class LdapManyObjectsInitializerCommand extends AbstractCommand {
private static LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username, private static LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
final String firstName, final String lastName, final String email, final String firstName, final String lastName, final String email,
String groupsDN, int startOffsetGroups, int countGroups) { 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) { UserModel helperUser = new UserModelDelegate(null) {
@ -181,7 +173,15 @@ public class LdapManyObjectsInitializerCommand extends AbstractCommand {
@Override @Override
public List<String> getAttribute(String name) { 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<>(); List<String> groupNamesList = new ArrayList<>();
for (int i=startOffsetGroups ; i<startOffsetGroups + countGroups ; i++) { for (int i=startOffsetGroups ; i<startOffsetGroups + countGroups ; i++) {