Introduce filtered (and paginated) search for organization members
Closes #28844 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
parent
3e16af8c0f
commit
45e5e6cbbf
13 changed files with 246 additions and 14 deletions
|
@ -25,6 +25,7 @@ import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
@ -36,10 +37,35 @@ public interface OrganizationMembersResource {
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
Response addMember(String userId);
|
Response addMember(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all members in the organization.
|
||||||
|
*
|
||||||
|
* @return a list containing the organization members.
|
||||||
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
List<UserRepresentation> getAll();
|
List<UserRepresentation> getAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all organization members that match the specified filters.
|
||||||
|
*
|
||||||
|
* @param search a {@code String} representing either a member's username, e-mail, first name, or last name.
|
||||||
|
* @param exact if {@code true}, the members will be searched using exact match for the {@code search} param - i.e.
|
||||||
|
* at least one of the username main attributes must match exactly the {@code search} param. If false,
|
||||||
|
* the method returns all members with at least one main attribute partially matching the {@code search} param.
|
||||||
|
* @param first index of the first element (pagination offset).
|
||||||
|
* @param max the maximum number of results.
|
||||||
|
* @return a list containing the matched organization members.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
List<UserRepresentation> search(
|
||||||
|
@QueryParam("search") String search,
|
||||||
|
@QueryParam("exact") Boolean exact,
|
||||||
|
@QueryParam("first") Integer first,
|
||||||
|
@QueryParam("max") Integer max
|
||||||
|
);
|
||||||
|
|
||||||
@Path("{id}/organization")
|
@Path("{id}/organization")
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -55,8 +55,8 @@ public interface OrganizationsResource {
|
||||||
* @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e.
|
* @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e.
|
||||||
* either the organization name or one of its domains must match exactly the {@code search} param. If false,
|
* either the organization name or one of its domains must match exactly the {@code search} param. If false,
|
||||||
* the method returns all organizations whose name or (domains) partially match the {@code search} param.
|
* the method returns all organizations whose name or (domains) partially match the {@code search} param.
|
||||||
* @param first index of the first element (pagination offset).
|
* @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}.
|
||||||
* @param max the maximum number of results.
|
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
|
||||||
* @return a list containing the matched organizations.
|
* @return a list containing the matched organizations.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
|
|
@ -472,6 +472,11 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
||||||
return getDelegate().getGroupMembersStream(realm, group, firstResult, maxResults);
|
return getDelegate().getGroupMembersStream(realm, group, firstResult, maxResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, String search, Boolean exact, Integer firstResult, Integer maxResults) {
|
||||||
|
return getDelegate().getGroupMembersStream(realm, group, search, exact, firstResult, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group) {
|
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group) {
|
||||||
return getDelegate().getGroupMembersStream(realm, group);
|
return getDelegate().getGroupMembersStream(realm, group);
|
||||||
|
|
|
@ -62,6 +62,7 @@ import jakarta.persistence.criteria.Predicate;
|
||||||
import jakarta.persistence.criteria.Root;
|
import jakarta.persistence.criteria.Root;
|
||||||
import jakarta.persistence.criteria.Subquery;
|
import jakarta.persistence.criteria.Subquery;
|
||||||
import org.keycloak.storage.jpa.JpaHashUtils;
|
import org.keycloak.storage.jpa.JpaHashUtils;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -710,6 +711,23 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
||||||
return closing(paginateQuery(query, firstResult, maxResults).getResultStream().map(user -> new UserAdapter(session, realm, em, user)));
|
return closing(paginateQuery(query, firstResult, maxResults).getResultStream().map(user -> new UserAdapter(session, realm, em, user)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, String search, Boolean exact, Integer first, Integer max) {
|
||||||
|
TypedQuery<UserEntity> query;
|
||||||
|
if (StringUtil.isBlank(search)) {
|
||||||
|
query = em.createNamedQuery("groupMembership", UserEntity.class);
|
||||||
|
} else if (Boolean.TRUE.equals(exact)) {
|
||||||
|
query = em.createNamedQuery("groupMembershipByUser", UserEntity.class);
|
||||||
|
query.setParameter("search", search);
|
||||||
|
} else {
|
||||||
|
query = em.createNamedQuery("groupMembershipByUserContained", UserEntity.class);
|
||||||
|
query.setParameter("search", search.toLowerCase());
|
||||||
|
}
|
||||||
|
query.setParameter("groupId", group.getId());
|
||||||
|
|
||||||
|
return closing(paginateQuery(query, first, max).getResultStream().map(user -> new UserAdapter(session, realm, em, user)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
|
||||||
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
|
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
|
||||||
|
|
|
@ -37,6 +37,11 @@ import java.io.Serializable;
|
||||||
@NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"),
|
@NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"),
|
||||||
@NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"),
|
@NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"),
|
||||||
@NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId order by g.user.username"),
|
@NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId order by g.user.username"),
|
||||||
|
@NamedQuery(name="groupMembershipByUser", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId and " +
|
||||||
|
"(g.user.username = :search or g.user.email = :search or g.user.firstName = :search or g.user.lastName = :search) order by g.user.username"),
|
||||||
|
@NamedQuery(name="groupMembershipByUserContained", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId and " +
|
||||||
|
"(g.user.username like concat('%',:search,'%') or g.user.email like concat('%',:search,'%') or lower(g.user.firstName) like concat('%',:search,'%') or " +
|
||||||
|
"lower(g.user.lastName) like concat('%',:search,'%')) order by g.user.username"),
|
||||||
@NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"),
|
@NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"),
|
||||||
@NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
|
@NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
|
||||||
@NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"),
|
@NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"),
|
||||||
|
|
|
@ -171,11 +171,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getMembersStream(OrganizationModel organization) {
|
public Stream<UserModel> getMembersStream(OrganizationModel organization, String search, Boolean exact, Integer first, Integer max) {
|
||||||
throwExceptionIfObjectIsNull(organization, "Organization");
|
throwExceptionIfObjectIsNull(organization, "Organization");
|
||||||
GroupModel group = getOrganizationGroup(organization);
|
GroupModel group = getOrganizationGroup(organization);
|
||||||
|
|
||||||
return userProvider.getGroupMembersStream(realm, group);
|
return userProvider.getGroupMembersStream(realm, group, search, exact, first, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -65,6 +65,7 @@ import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
|
||||||
import org.keycloak.storage.client.ClientStorageProvider;
|
import org.keycloak.storage.client.ClientStorageProvider;
|
||||||
import org.keycloak.storage.datastore.DefaultDatastoreProvider;
|
import org.keycloak.storage.datastore.DefaultDatastoreProvider;
|
||||||
import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
||||||
|
import org.keycloak.storage.federated.UserGroupMembershipFederatedStorage;
|
||||||
import org.keycloak.storage.managers.UserStorageSyncManager;
|
import org.keycloak.storage.managers.UserStorageSyncManager;
|
||||||
import org.keycloak.storage.user.ImportedUserValidation;
|
import org.keycloak.storage.user.ImportedUserValidation;
|
||||||
import org.keycloak.storage.user.UserBulkUpdateProvider;
|
import org.keycloak.storage.user.UserBulkUpdateProvider;
|
||||||
|
@ -76,6 +77,8 @@ import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
import org.keycloak.userprofile.AttributeMetadata;
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileDecorator;
|
import org.keycloak.userprofile.UserProfileDecorator;
|
||||||
import org.keycloak.userprofile.UserProfileMetadata;
|
import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
|
import org.keycloak.utils.StreamsUtil;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -423,6 +426,36 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
||||||
return importValidation(realm, results);
|
return importValidation(realm, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> getGroupMembersStream(final RealmModel realm, final GroupModel group, final String search,
|
||||||
|
final Boolean exact, final Integer firstResult, final Integer maxResults) {
|
||||||
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
|
if (provider instanceof UserQueryMethodsProvider) {
|
||||||
|
return ((UserQueryMethodsProvider)provider).getGroupMembersStream(realm, group, search, exact, firstResultInQuery, maxResultsInQuery);
|
||||||
|
|
||||||
|
} else if (provider instanceof UserFederatedStorageProvider) {
|
||||||
|
// modify this if UserGroupMembershipFederatedStorage adds a getMembershipStream variant with search option.
|
||||||
|
return StreamsUtil.paginatedStream(((UserFederatedStorageProvider)provider).getMembershipStream(realm, group, null, null)
|
||||||
|
.map(id -> getUserById(realm, id))
|
||||||
|
.filter(user -> {
|
||||||
|
if (StringUtil.isBlank(search)) return true;
|
||||||
|
if (Boolean.TRUE.equals(exact)) {
|
||||||
|
return search.equals(user.getUsername()) || search.equals(user.getEmail())
|
||||||
|
|| search.equals(user.getFirstName()) || search.equals(user.getLastName());
|
||||||
|
} else {
|
||||||
|
return Optional.ofNullable(user.getUsername()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getEmail()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getFirstName()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getLastName()).orElse("").toLowerCase().contains(search.toLowerCase());
|
||||||
|
}
|
||||||
|
}), firstResultInQuery, maxResultsInQuery);
|
||||||
|
}
|
||||||
|
return Stream.empty();
|
||||||
|
}, realm, firstResult, maxResults);
|
||||||
|
|
||||||
|
return importValidation(realm, results);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) {
|
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) {
|
||||||
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
Stream<UserModel> results = query((provider, firstResultInQuery, maxResultsInQuery) -> {
|
||||||
|
|
|
@ -72,8 +72,8 @@ public interface OrganizationProvider extends Provider {
|
||||||
* @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e.
|
* @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e.
|
||||||
* either the organization name or one of its domains must match exactly the {@code search} param. If false,
|
* either the organization name or one of its domains must match exactly the {@code search} param. If false,
|
||||||
* the method returns all organizations whose name or (domains) partially match the {@code search} param.
|
* the method returns all organizations whose name or (domains) partially match the {@code search} param.
|
||||||
* @param first index of the first element (pagination offset).
|
* @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}.
|
||||||
* @param max the maximum number of results.
|
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
|
||||||
* @return a {@link Stream} of the matched organizations. Never returns {@code null}.
|
* @return a {@link Stream} of the matched organizations. Never returns {@code null}.
|
||||||
*/
|
*/
|
||||||
Stream<OrganizationModel> getAllStream(String search, Boolean exact, Integer first, Integer max);
|
Stream<OrganizationModel> getAllStream(String search, Boolean exact, Integer first, Integer max);
|
||||||
|
@ -103,12 +103,12 @@ public interface OrganizationProvider extends Provider {
|
||||||
boolean addMember(OrganizationModel organization, UserModel user);
|
boolean addMember(OrganizationModel organization, UserModel user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the members of a given {@link OrganizationModel}.
|
* Returns the members of a given {@link OrganizationModel} filtered according to the specified parameters.
|
||||||
*
|
*
|
||||||
* @param organization the organization
|
* @param organization the organization
|
||||||
* @return Stream of the members. Never returns {@code null}.
|
* @return Stream of the members. Never returns {@code null}.
|
||||||
*/
|
*/
|
||||||
Stream<UserModel> getMembersStream(OrganizationModel organization);
|
Stream<UserModel> getMembersStream(OrganizationModel organization, String search, Boolean exact, Integer first, Integer max);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the member of the {@link OrganizationModel} by its {@code id}.
|
* Returns the member of the {@link OrganizationModel} by its {@code id}.
|
||||||
|
|
|
@ -21,8 +21,10 @@ import org.keycloak.models.GroupModel;
|
||||||
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.utils.StringUtil;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -160,6 +162,45 @@ public interface UserQueryMethodsProvider {
|
||||||
*/
|
*/
|
||||||
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
|
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains users that belong to a specific group, filtered according to the search parameters.
|
||||||
|
*
|
||||||
|
* @param realm a reference to the realm.
|
||||||
|
* @param group a reference to the group.
|
||||||
|
* @param search the search string. It can represent either the user's username, e-mail, first name, or last name.
|
||||||
|
* @param exact a boolean indicating if the search should be exact or not. If {@code true}, it selects only users
|
||||||
|
* whose main attributes (username, e-mail, first name, or last name) exactly match the search string.
|
||||||
|
* If {@code false}, it selects the users whose main attributes partially match the search string.
|
||||||
|
* @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}.
|
||||||
|
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
|
||||||
|
* @return a non-null {@link Stream} of filtered users that belong to the group.
|
||||||
|
*/
|
||||||
|
default Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, String search, Boolean exact,
|
||||||
|
Integer first, Integer max) {
|
||||||
|
|
||||||
|
Stream<UserModel> groupMembers = getGroupMembersStream(realm, group).filter(user -> {
|
||||||
|
if (StringUtil.isBlank(search)) return true;
|
||||||
|
if (Boolean.TRUE.equals(exact)) {
|
||||||
|
return search.equals(user.getUsername()) || search.equals(user.getEmail())
|
||||||
|
|| search.equals(user.getFirstName()) || search.equals(user.getLastName());
|
||||||
|
} else {
|
||||||
|
return Optional.ofNullable(user.getUsername()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getEmail()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getFirstName()).orElse("").toLowerCase().contains(search.toLowerCase()) ||
|
||||||
|
Optional.ofNullable(user.getLastName()).orElse("").toLowerCase().contains(search.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copied over from StreamsUtil from server-spi-private which is not available here
|
||||||
|
if (first != null && first > 0) {
|
||||||
|
groupMembers = groupMembers.skip(first);
|
||||||
|
}
|
||||||
|
if (max != null && max >= 0) {
|
||||||
|
groupMembers = groupMembers.limit(max);
|
||||||
|
}
|
||||||
|
return groupMembers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains users that have the specified role.
|
* Obtains users that have the specified role.
|
||||||
*
|
*
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DELETE;
|
import jakarta.ws.rs.DELETE;
|
||||||
|
import jakarta.ws.rs.DefaultValue;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
|
@ -28,11 +29,15 @@ import jakarta.ws.rs.PUT;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.ext.Provider;
|
import jakarta.ws.rs.ext.Provider;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
@ -100,9 +105,15 @@ public class OrganizationMemberResource {
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Stream<UserRepresentation> getMembers() {
|
@Operation( summary = "Return a paginated list of organization members filtered according to the specified parameters")
|
||||||
|
public Stream<UserRepresentation> search(
|
||||||
|
@Parameter(description = "A String representing either a member's username, e-mail, first name, or last name.") @QueryParam("search") String search,
|
||||||
|
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact,
|
||||||
|
@Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first,
|
||||||
|
@Parameter(description = "The maximum number of results to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max
|
||||||
|
) {
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
return provider.getMembersStream(organization).map(this::toRepresentation);
|
return provider.getMembersStream(organization, search, exact, first, max).map(this::toRepresentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
|
|
|
@ -91,9 +91,9 @@ public class OrganizationResource {
|
||||||
@Operation( summary = "Return a paginated list of organizations filtered according to the specified parameters")
|
@Operation( summary = "Return a paginated list of organizations filtered according to the specified parameters")
|
||||||
public Stream<OrganizationRepresentation> search(
|
public Stream<OrganizationRepresentation> search(
|
||||||
@Parameter(description = "A String representing either an organization name or domain") @QueryParam("search") String search,
|
@Parameter(description = "A String representing either an organization name or domain") @QueryParam("search") String search,
|
||||||
@Parameter(description = "Boolean which defines whether the params \"search\" must match exactly or not") @QueryParam("exact") Boolean exact,
|
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact,
|
||||||
@Parameter(description = "The position of the first result to be returned (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first,
|
@Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first,
|
||||||
@Parameter(description = "The maximum number of results that are to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max
|
@Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max
|
||||||
) {
|
) {
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
return provider.getAllStream(search, exact, first, max).map(this::toRepresentation);
|
return provider.getAllStream(search, exact, first, max).map(this::toRepresentation);
|
||||||
|
|
|
@ -151,11 +151,17 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserRepresentation addMember(OrganizationResource organization, String email) {
|
protected UserRepresentation addMember(OrganizationResource organization, String email) {
|
||||||
|
return addMember(organization, email, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRepresentation addMember(OrganizationResource organization, String email, String firstName, String lastName) {
|
||||||
UserRepresentation expected = new UserRepresentation();
|
UserRepresentation expected = new UserRepresentation();
|
||||||
|
|
||||||
expected.setEmail(email);
|
expected.setEmail(email);
|
||||||
expected.setUsername(expected.getEmail());
|
expected.setUsername(expected.getEmail());
|
||||||
expected.setEnabled(true);
|
expected.setEnabled(true);
|
||||||
|
expected.setFirstName(firstName);
|
||||||
|
expected.setLastName(lastName);
|
||||||
Users.setPasswordFor(expected, memberPassword);
|
Users.setPasswordFor(expected, memberPassword);
|
||||||
|
|
||||||
try (Response response = testRealm().users().create(expected)) {
|
try (Response response = testRealm().users().create(expected)) {
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
package org.keycloak.testsuite.organization.admin;
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
@ -145,7 +149,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
|
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<UserRepresentation> existing = organization.members().getAll();;
|
List<UserRepresentation> existing = organization.members().getAll();
|
||||||
assertFalse(existing.isEmpty());
|
assertFalse(existing.isEmpty());
|
||||||
assertEquals(expected.size(), existing.size());
|
assertEquals(expected.size(), existing.size());
|
||||||
for (UserRepresentation expectedRep : expected) {
|
for (UserRepresentation expectedRep : expected) {
|
||||||
|
@ -228,4 +232,87 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
assertFalse(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
|
assertFalse(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchMembers() {
|
||||||
|
|
||||||
|
// create test users, ordered by username (e-mail).
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
List<UserRepresentation> expected = new ArrayList<>();
|
||||||
|
expected.add(addMember(organization, "batwoman@neworg.org", "Katherine", "Kane"));
|
||||||
|
expected.add(addMember(organization, "brucewayne@neworg.org", "Bruce", "Wayne"));
|
||||||
|
expected.add(addMember(organization, "harveydent@neworg.org", "Harvey", "Dent"));
|
||||||
|
expected.add(addMember(organization, "marthaw@neworg.org", "Martha", "Wayne"));
|
||||||
|
expected.add(addMember(organization, "thejoker@neworg.org", "Jack", "White"));
|
||||||
|
|
||||||
|
// exact search - username/e-mail, first name, last name.
|
||||||
|
List<UserRepresentation> existing = organization.members().search("brucewayne@neworg.org", true, null, null);
|
||||||
|
assertThat(existing, hasSize(1));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getEmail(), is(equalTo("brucewayne@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getFirstName(), is(equalTo("Bruce")));
|
||||||
|
assertThat(existing.get(0).getLastName(), is(equalTo("Wayne")));
|
||||||
|
|
||||||
|
existing = organization.members().search("Harvey", true, null, null);
|
||||||
|
assertThat(existing, hasSize(1));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("harveydent@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getEmail(), is(equalTo("harveydent@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getFirstName(), is(equalTo("Harvey")));
|
||||||
|
assertThat(existing.get(0).getLastName(), is(equalTo("Dent")));
|
||||||
|
|
||||||
|
existing = organization.members().search("Wayne", true, null, null);
|
||||||
|
assertThat(existing, hasSize(2));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org")));
|
||||||
|
assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org")));
|
||||||
|
|
||||||
|
existing = organization.members().search("Gordon", true, null, null);
|
||||||
|
assertThat(existing, is(empty()));
|
||||||
|
|
||||||
|
// partial search - partial e-mail should match all users.
|
||||||
|
existing = organization.members().search("neworg", false, null, null);
|
||||||
|
assertThat(existing, hasSize(5));
|
||||||
|
for (int i = 0; i < 5; i++) { // returned entries should also be ordered.
|
||||||
|
assertThat(expected.get(i).getId(), is(equalTo(expected.get(i).getId())));
|
||||||
|
assertThat(expected.get(i).getUsername(), is(equalTo(expected.get(i).getUsername())));
|
||||||
|
assertThat(expected.get(i).getEmail(), is(equalTo(expected.get(i).getEmail())));
|
||||||
|
assertThat(expected.get(i).getFirstName(), is(equalTo(expected.get(i).getFirstName())));
|
||||||
|
assertThat(expected.get(i).getLastName(), is(equalTo(expected.get(i).getLastName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// partial search using 'th' search string - should match 'Katherine' by name, 'Jack' by username/e-mail
|
||||||
|
// and 'Martha' either by username or first name.
|
||||||
|
existing = organization.members().search("th", false, null, null);
|
||||||
|
assertThat(existing, hasSize(3));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("batwoman@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getFirstName(), is(equalTo("Katherine")));
|
||||||
|
assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org")));
|
||||||
|
assertThat(existing.get(1).getFirstName(), is(equalTo("Martha")));
|
||||||
|
assertThat(existing.get(2).getUsername(), is(equalTo("thejoker@neworg.org")));
|
||||||
|
assertThat(existing.get(2).getFirstName(), is(equalTo("Jack")));
|
||||||
|
|
||||||
|
// partial search using 'way' - should match both 'Bruce' (either by username or last name) and 'Martha' by last name.
|
||||||
|
existing = organization.members().search("way", false, null, null);
|
||||||
|
assertThat(existing, hasSize(2));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org")));
|
||||||
|
assertThat(existing.get(0).getFirstName(), is(equalTo("Bruce")));
|
||||||
|
assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org")));
|
||||||
|
assertThat(existing.get(1).getFirstName(), is(equalTo("Martha")));
|
||||||
|
|
||||||
|
// partial search using with no match - e.g. 'nonexistent'.
|
||||||
|
existing = organization.members().search("nonexistent", false, null, null);
|
||||||
|
assertThat(existing, is(empty()));
|
||||||
|
|
||||||
|
// paginated search - try to fetch 3 users per page.
|
||||||
|
existing = organization.members().search("", false, 0, 3);
|
||||||
|
assertThat(existing, hasSize(3));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("batwoman@neworg.org")));
|
||||||
|
assertThat(existing.get(1).getUsername(), is(equalTo("brucewayne@neworg.org")));
|
||||||
|
assertThat(existing.get(2).getUsername(), is(equalTo("harveydent@neworg.org")));
|
||||||
|
|
||||||
|
existing = organization.members().search("", false, 3, 3);
|
||||||
|
assertThat(existing, hasSize(2));
|
||||||
|
assertThat(existing.get(0).getUsername(), is(equalTo("marthaw@neworg.org")));
|
||||||
|
assertThat(existing.get(1).getUsername(), is(equalTo("thejoker@neworg.org")));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue