[KEYCLOAK-18891] Add support for searching users by custom user attributes

Users can now be searched by custom attributes using 'q' in the query parameters. The implementation is roughly the same as search clients by custom attributes.
This commit is contained in:
Bart Monhemius 2021-07-29 10:20:07 +02:00 committed by Hynek Mlnařík
parent ce0070508f
commit 5b0986e490
6 changed files with 133 additions and 30 deletions

View file

@ -109,6 +109,20 @@ public interface UsersResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username); List<UserRepresentation> search(@QueryParam("username") String username);
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery);
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
List<UserRepresentation> searchByAttributes(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("enabled") Boolean enabled,
@QueryParam("briefRepresentation") Boolean briefRepresentation,
@QueryParam("q") String searchQuery);
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username, @QueryParam("exact") Boolean exact); List<UserRepresentation> search(@QueryParam("username") String username, @QueryParam("exact") Boolean exact);
@ -246,7 +260,7 @@ public interface UsersResource {
@Path("{id}") @Path("{id}")
@DELETE @DELETE
Response delete(@PathParam("id") String id); Response delete(@PathParam("id") String id);
@Path("profile") @Path("profile")
UserProfileResource userProfile(); UserProfileResource userProfile();

View file

@ -39,6 +39,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.CredentialEntity; import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.jpa.entities.FederatedIdentityEntity; import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity; import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity;
import org.keycloak.models.jpa.entities.UserConsentEntity; import org.keycloak.models.jpa.entities.UserConsentEntity;
import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.jpa.entities.UserEntity;
@ -49,15 +50,16 @@ 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.LockModeType;
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;
import javax.persistence.criteria.Expression; import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join; import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery; import javax.persistence.criteria.Subquery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
@ -68,8 +70,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.persistence.LockModeType;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing; import static org.keycloak.utils.StreamsUtil.closing;
@ -768,7 +768,8 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
CriteriaQuery<UserEntity> queryBuilder = builder.createQuery(UserEntity.class); CriteriaQuery<UserEntity> queryBuilder = builder.createQuery(UserEntity.class);
Root<UserEntity> root = queryBuilder.from(UserEntity.class); Root<UserEntity> root = queryBuilder.from(UserEntity.class);
List<Predicate> predicates = new ArrayList(); List<Predicate> predicates = new ArrayList<>();
List<Predicate> attributePredicates = new ArrayList<>();
predicates.add(builder.equal(root.get("realmId"), realm.getId())); predicates.add(builder.equal(root.get("realmId"), realm.getId()));
@ -788,7 +789,7 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
switch (key) { switch (key) {
case UserModel.SEARCH: case UserModel.SEARCH:
List<Predicate> orPredicates = new ArrayList(); List<Predicate> orPredicates = new ArrayList<>();
orPredicates orPredicates
.add(builder.like(builder.lower(root.get(USERNAME)), "%" + value.toLowerCase() + "%")); .add(builder.like(builder.lower(root.get(USERNAME)), "%" + value.toLowerCase() + "%"));
@ -799,7 +800,7 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
builder.coalesce(root.get(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[0])));
break; break;
@ -831,9 +832,24 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
} }
predicates.add(builder.equal(federatedIdentitiesJoin.get("userId"), value)); predicates.add(builder.equal(federatedIdentitiesJoin.get("userId"), value));
break; break;
case UserModel.EXACT:
break;
// All unknown attributes will be assumed as custom attributes
default:
Join<UserEntity, UserAttributeEntity> attributesJoin = root.join("attributes", JoinType.LEFT);
attributePredicates.add(builder.and(
builder.equal(builder.lower(attributesJoin.get("name")), key.toLowerCase()),
builder.equal(builder.lower(attributesJoin.get("value")), value.toLowerCase())));
break;
} }
} }
if (!attributePredicates.isEmpty()) {
predicates.add(builder.and(attributePredicates.toArray(new Predicate[0])));
}
Set<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS); Set<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS);
if (userGroups != null) { if (userGroups != null) {

View file

@ -637,6 +637,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
mcb = mcb.compare(SearchableFields.IDP_AND_USER, Operator.EQ, attributes.get(UserModel.IDP_ALIAS), value); mcb = mcb.compare(SearchableFields.IDP_AND_USER, Operator.EQ, attributes.get(UserModel.IDP_ALIAS), value);
break; break;
} }
case UserModel.EXACT:
break;
default:
mcb = mcb.compare(SearchableFields.ATTRIBUTE, Operator.EQ, key, value);
break;
} }
} }

View file

@ -389,6 +389,8 @@ public interface UserQueryProvider {
* the given userId (case sensitive string)</li> * the given userId (case sensitive string)</li>
* </ul> * </ul>
* *
* Any other parameters will be treated as custom user attributes.
*
* This method is used by the REST API when querying users. * This method is used by the REST API when querying users.
* *
* @param realm a reference to the realm. * @param realm a reference to the realm.

View file

@ -16,8 +16,6 @@
*/ */
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import static org.keycloak.userprofile.UserProfileContext.USER_API;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
@ -43,6 +41,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator;
import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.utils.SearchQueryUtils;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -56,11 +55,14 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.keycloak.userprofile.UserProfileContext.USER_API;
/** /**
* Base resource for managing users * Base resource for managing users
* *
@ -215,7 +217,7 @@ public class UsersResource {
/** /**
* Get users * Get users
* *
* Returns a stream of users, filtered according to query parameters * Returns a stream of users, filtered according to query parameters.
* *
* @param search A String contained in username, first or last name, or email * @param search A String contained in username, first or last name, or email
* @param last A String contained in lastName, or the complete lastName, if param "exact" is true * @param last A String contained in lastName, or the complete lastName, if param "exact" is true
@ -230,6 +232,7 @@ public class UsersResource {
* @param enabled Boolean representing if user is enabled or not * @param enabled Boolean representing if user is enabled or not
* @param briefRepresentation Boolean which defines whether brief representations are returned (default: false) * @param briefRepresentation Boolean which defines whether brief representations are returned (default: false)
* @param exact Boolean which defines whether the params "last", "first", "email" and "username" must match exactly * @param exact Boolean which defines whether the params "last", "first", "email" and "username" must match exactly
* @param searchQuery A query to search for custom attributes, in the format 'key1:value2 key2:value2'
* @return a non-null {@code Stream} of users * @return a non-null {@code Stream} of users
*/ */
@GET @GET
@ -247,7 +250,8 @@ public class UsersResource {
@QueryParam("max") Integer maxResults, @QueryParam("max") Integer maxResults,
@QueryParam("enabled") Boolean enabled, @QueryParam("enabled") Boolean enabled,
@QueryParam("briefRepresentation") Boolean briefRepresentation, @QueryParam("briefRepresentation") Boolean briefRepresentation,
@QueryParam("exact") Boolean exact) { @QueryParam("exact") Boolean exact,
@QueryParam("q") String searchQuery) {
UserPermissionEvaluator userPermissionEvaluator = auth.users(); UserPermissionEvaluator userPermissionEvaluator = auth.users();
userPermissionEvaluator.requireQuery(); userPermissionEvaluator.requireQuery();
@ -255,6 +259,10 @@ public class UsersResource {
firstResult = firstResult != null ? firstResult : -1; firstResult = firstResult != null ? firstResult : -1;
maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS;
Map<String, String> searchAttributes = searchQuery == null
? Collections.emptyMap()
: SearchQueryUtils.getFields(searchQuery);
Stream<UserModel> userModels = Stream.empty(); Stream<UserModel> userModels = Stream.empty();
if (search != null) { if (search != null) {
if (search.startsWith(SEARCH_ID_PARAMETER)) { if (search.startsWith(SEARCH_ID_PARAMETER)) {
@ -273,7 +281,7 @@ public class UsersResource {
maxResults, false); maxResults, false);
} }
} else if (last != null || first != null || email != null || username != null || emailVerified != null } else if (last != null || first != null || email != null || username != null || emailVerified != null
|| idpAlias != null || idpUserId != null || enabled != null || exact != null) { || idpAlias != null || idpUserId != null || enabled != null || exact != null || !searchAttributes.isEmpty()) {
Map<String, String> attributes = new HashMap<>(); Map<String, String> attributes = new HashMap<>();
if (last != null) { if (last != null) {
attributes.put(UserModel.LAST_NAME, last); attributes.put(UserModel.LAST_NAME, last);
@ -302,6 +310,9 @@ public class UsersResource {
if (exact != null) { if (exact != null) {
attributes.put(UserModel.EXACT, exact.toString()); attributes.put(UserModel.EXACT, exact.toString());
} }
attributes.putAll(searchAttributes);
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult,
maxResults, true); maxResults, true);
} else { } else {

View file

@ -61,6 +61,8 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
import org.keycloak.testsuite.page.LoginPasswordUpdatePage; import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
@ -101,6 +103,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -116,9 +119,6 @@ import static org.junit.Assert.fail;
import static org.keycloak.testsuite.Assert.assertNames; import static org.keycloak.testsuite.Assert.assertNames;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -595,6 +595,12 @@ public class UserTest extends AbstractAdminTest {
user.setFirstName("First" + i); user.setFirstName("First" + i);
user.setLastName("Last" + i); user.setLastName("Last" + i);
HashMap<String, List<String>> attributes = new HashMap<>();
attributes.put("test", Collections.singletonList("test" + i));
attributes.put("test" + i, Collections.singletonList("test" + i));
attributes.put("attr", Collections.singletonList("common"));
user.setAttributes(attributes);
ids.add(createUser(user)); ids.add(createUser(user));
} }
@ -623,15 +629,64 @@ public class UserTest extends AbstractAdminTest {
assertEquals(9, users.size()); assertEquals(9, users.size());
} }
private String mapToSearchQuery(Map<String, String> search) {
return search.entrySet()
.stream()
.map(e -> String.format("%s:%s", e.getKey(), e.getValue()))
.collect(Collectors.joining(" "));
}
@Test
public void searchByAttribute() {
createUsers();
Map<String, String> attributes = new HashMap<>();
attributes.put("test", "test1");
List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(attributes));
assertEquals(1, users.size());
attributes.clear();
attributes.put("attr", "common");
users = realm.users().searchByAttributes(mapToSearchQuery(attributes));
assertEquals(9, users.size());
}
@Test
public void searchByMultipleAttributes() {
createUsers();
Map<String, String> attributes = new HashMap<>();
attributes.put("test", "test1");
attributes.put("attr", "common");
attributes.put("test1", "test1");
List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(attributes));
assertEquals(1, users.size());
}
@Test
public void searchByAttributesWithPagination() {
createUsers();
Map<String, String> attributes = new HashMap<>();
attributes.put("attr", "common");
for (int i = 1; i < 10; i++) {
List<UserRepresentation> users = realm.users().searchByAttributes(i - 1, 1, null, false, mapToSearchQuery(attributes));
assertEquals(1, users.size());
assertTrue(users.get(0).getAttributes().keySet().stream().anyMatch(attributes::containsKey));
}
}
@Test @Test
public void searchByUsernameExactMatch() { public void searchByUsernameExactMatch() {
createUsers(); createUsers();
UserRepresentation user = new UserRepresentation(); UserRepresentation user = new UserRepresentation();
user.setUsername("username11"); user.setUsername("username11");
createUser(user); createUser(user);
List<UserRepresentation> users = realm.users().search("username1", true); List<UserRepresentation> users = realm.users().search("username1", true);
assertEquals(1, users.size()); assertEquals(1, users.size());
@ -2022,14 +2077,14 @@ public class UserTest extends AbstractAdminTest {
realm.flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString(), updatePasswordReqAction); realm.flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString(), updatePasswordReqAction);
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.authRequiredActionPath(UserModel.RequiredAction.UPDATE_PASSWORD.toString()), updatePasswordReqAction, ResourceType.REQUIRED_ACTION); assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.authRequiredActionPath(UserModel.RequiredAction.UPDATE_PASSWORD.toString()), updatePasswordReqAction, ResourceType.REQUIRED_ACTION);
} }
private RoleRepresentation getRoleByName(String name, List<RoleRepresentation> roles) { private RoleRepresentation getRoleByName(String name, List<RoleRepresentation> roles) {
for(RoleRepresentation role : roles) { for(RoleRepresentation role : roles) {
if(role.getName().equalsIgnoreCase(name)) { if(role.getName().equalsIgnoreCase(name)) {
return role; return role;
} }
} }
return null; return null;
} }
@ -2042,7 +2097,7 @@ public class UserTest extends AbstractAdminTest {
realm.update(realmRep); realm.update(realmRep);
RoleRepresentation realmCompositeRole = RoleBuilder.create().name("realm-composite").singleAttribute("attribute1", "value1").build(); RoleRepresentation realmCompositeRole = RoleBuilder.create().name("realm-composite").singleAttribute("attribute1", "value1").build();
realm.roles().create(RoleBuilder.create().name("realm-role").build()); realm.roles().create(RoleBuilder.create().name("realm-role").build());
realm.roles().create(realmCompositeRole); realm.roles().create(realmCompositeRole);
realm.roles().create(RoleBuilder.create().name("realm-child").build()); realm.roles().create(RoleBuilder.create().name("realm-child").build());
@ -2054,8 +2109,8 @@ public class UserTest extends AbstractAdminTest {
response.close(); response.close();
RoleRepresentation clientCompositeRole = RoleBuilder.create().name("client-composite").singleAttribute("attribute1", "value1").build(); RoleRepresentation clientCompositeRole = RoleBuilder.create().name("client-composite").singleAttribute("attribute1", "value1").build();
realm.clients().get(clientUuid).roles().create(RoleBuilder.create().name("client-role").build()); realm.clients().get(clientUuid).roles().create(RoleBuilder.create().name("client-role").build());
realm.clients().get(clientUuid).roles().create(RoleBuilder.create().name("client-role2").build()); realm.clients().get(clientUuid).roles().create(RoleBuilder.create().name("client-role2").build());
realm.clients().get(clientUuid).roles().create(clientCompositeRole); realm.clients().get(clientUuid).roles().create(clientCompositeRole);
@ -2099,12 +2154,12 @@ public class UserTest extends AbstractAdminTest {
RoleRepresentation realmCompositeRoleFromList = getRoleByName("realm-composite", realmRolesFullRepresentations); RoleRepresentation realmCompositeRoleFromList = getRoleByName("realm-composite", realmRolesFullRepresentations);
assertNotNull(realmCompositeRoleFromList); assertNotNull(realmCompositeRoleFromList);
assertTrue(realmCompositeRoleFromList.getAttributes().containsKey("attribute1")); assertTrue(realmCompositeRoleFromList.getAttributes().containsKey("attribute1"));
// List client roles // List client roles
assertNames(roles.clientLevel(clientUuid).listAll(), "client-role", "client-composite"); assertNames(roles.clientLevel(clientUuid).listAll(), "client-role", "client-composite");
assertNames(roles.clientLevel(clientUuid).listAvailable(), "client-role2", "client-child"); assertNames(roles.clientLevel(clientUuid).listAvailable(), "client-role2", "client-child");
assertNames(roles.clientLevel(clientUuid).listEffective(), "client-role", "client-composite", "client-child"); assertNames(roles.clientLevel(clientUuid).listEffective(), "client-role", "client-composite", "client-child");
// List client effective role with full representation // List client effective role with full representation
List<RoleRepresentation> rolesFullRepresentations = roles.clientLevel(clientUuid).listEffective(false); List<RoleRepresentation> rolesFullRepresentations = roles.clientLevel(clientUuid).listEffective(false);
RoleRepresentation clientCompositeRoleFromList = getRoleByName("client-composite", rolesFullRepresentations); RoleRepresentation clientCompositeRoleFromList = getRoleByName("client-composite", rolesFullRepresentations);
@ -2455,11 +2510,11 @@ public class UserTest extends AbstractAdminTest {
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel())); Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel()));
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getPriority(), otpCredentialLoaded.getPriority())); Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getPriority(), otpCredentialLoaded.getPriority()));
} }
@Test @Test
public void testGetGroupsForUserFullRepresentation() { public void testGetGroupsForUserFullRepresentation() {
RealmResource realm = adminClient.realms().realm("test"); RealmResource realm = adminClient.realms().realm("test");
String userName = "averagejoe"; String userName = "averagejoe";
String groupName = "groupWithAttribute"; String groupName = "groupWithAttribute";
Map<String, List<String>> attributes = new HashMap<String, List<String>>(); Map<String, List<String>> attributes = new HashMap<String, List<String>>();
@ -2469,16 +2524,16 @@ public class UserTest extends AbstractAdminTest {
.edit(createUserRepresentation(userName, "joe@average.com", "average", "joe", true)) .edit(createUserRepresentation(userName, "joe@average.com", "average", "joe", true))
.addPassword("password") .addPassword("password")
.build(); .build();
try (Creator<UserResource> u = Creator.create(realm, userRepresentation); try (Creator<UserResource> u = Creator.create(realm, userRepresentation);
Creator<GroupResource> g = Creator.create(realm, GroupBuilder.create().name(groupName).attributes(attributes).build())) { Creator<GroupResource> g = Creator.create(realm, GroupBuilder.create().name(groupName).attributes(attributes).build())) {
String groupId = g.id(); String groupId = g.id();
UserResource user = u.resource(); UserResource user = u.resource();
user.joinGroup(groupId); user.joinGroup(groupId);
List<GroupRepresentation> userGroups = user.groups(0, 100, false); List<GroupRepresentation> userGroups = user.groups(0, 100, false);
assertFalse(userGroups.isEmpty()); assertFalse(userGroups.isEmpty());
assertTrue(userGroups.get(0).getAttributes().containsKey("attribute1")); assertTrue(userGroups.get(0).getAttributes().containsKey("attribute1"));
} }