diff --git a/src/main/java/io/phasetwo/service/model/OrganizationMemberModel.java b/src/main/java/io/phasetwo/service/model/OrganizationMemberModel.java index 3906d1bb..dd2bb23f 100644 --- a/src/main/java/io/phasetwo/service/model/OrganizationMemberModel.java +++ b/src/main/java/io/phasetwo/service/model/OrganizationMemberModel.java @@ -1,7 +1,6 @@ package io.phasetwo.service.model; import java.util.List; -import java.util.Map; public interface OrganizationMemberModel extends WithAttributes { diff --git a/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java b/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java index 7bc1c9a8..b23095cb 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java +++ b/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java @@ -1,6 +1,10 @@ package io.phasetwo.service.model.jpa; import static io.phasetwo.service.Orgs.*; +import static org.keycloak.models.UserModel.EMAIL; +import static org.keycloak.models.UserModel.FIRST_NAME; +import static org.keycloak.models.UserModel.LAST_NAME; +import static org.keycloak.models.UserModel.USERNAME; import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; import static org.keycloak.utils.StreamsUtil.closing; @@ -20,18 +24,28 @@ import io.phasetwo.service.util.IdentityProviders; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; + +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.jpa.JpaModel; +import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.utils.KeycloakModelUtils; public class OrganizationAdapter implements OrganizationModel, JpaModel { @@ -41,6 +55,8 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel getOrganizationMembersStream() { @Override public Stream searchForOrganizationMembersStream(String search, Integer firstResult, Integer maxResults) { - TypedQuery query; - if(search != null && !search.isEmpty()) { - query = em.createNamedQuery("searchOrganizationMembers", OrganizationMemberEntity.class); - query.setParameter("organization", org); - query.setParameter("search", search); - } else { - query = em.createNamedQuery("getOrganizationMembers", OrganizationMemberEntity.class); - query.setParameter("organization", org); + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(OrganizationMemberEntity.class); + + Root root = criteriaQuery.from(OrganizationMemberEntity.class); + + List predicates = new ArrayList<>(); + // defining the organization search clause + predicates.add(criteriaBuilder.equal(root.get("organization"), org)); + if (search != null && !search.isEmpty()) { + var userIds = userIdsSubquery(criteriaQuery, search); + predicates.add(root.get("userId").in(userIds)); } + criteriaQuery.where(predicates.toArray(Predicate[]::new)).orderBy(criteriaBuilder.asc(root.get("createdAt"))); + + TypedQuery query = em.createQuery(criteriaQuery); + return closing(paginateQuery(query, firstResult, maxResults).getResultStream()) .filter(Objects::nonNull) .map(organizationMemberEntity -> new OrganizationMemberAdapter(session, realm, em, organizationMemberEntity)); } + private Subquery userIdsSubquery(CriteriaQuery query, String search) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + Subquery subquery = query.subquery(String.class); + Root subRoot = subquery.from(UserEntity.class); + + subquery.select(subRoot.get("id")); + List subqueryPredicates = new ArrayList<>(); + + subqueryPredicates.add(cb.equal(subRoot.get("realmId"), realm.getId())); + + List searchTermsPredicates = new ArrayList<>(); + //define search terms + for (String stringToSearch : search.trim().split(",")) { + searchTermsPredicates.add(cb.or(getSearchOptionPredicateArray(stringToSearch, cb, subRoot))); + } + Predicate searchPredicate = cb.or(searchTermsPredicates.toArray(Predicate[]::new)); + subqueryPredicates.add(searchPredicate); + + subquery.where(subqueryPredicates.toArray(Predicate[]::new)); + + return subquery; + } + @Override public Long getMembersCount() { TypedQuery query = em.createNamedQuery("getOrganizationMembersCount", Long.class); @@ -368,4 +414,17 @@ public Stream getIdentityProvidersStream() { return orgs.contains(getId()); }); } + + private Predicate[] getSearchOptionPredicateArray(String value, CriteriaBuilder builder, From from) { + value = value.trim().toLowerCase(); + List orPredicates = new ArrayList<>(); + if (!value.isEmpty()) { + value = "%" + value + "%"; //contains in SQL query manner + orPredicates.add(builder.like(from.get(USERNAME), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(from.get(EMAIL), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH)); + } + return orPredicates.toArray(Predicate[]::new); + } } diff --git a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java index e0542183..827cdfc6 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java +++ b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMemberEntity.java @@ -33,14 +33,6 @@ name = "getOrganizationMembers", query = "SELECT m FROM OrganizationMemberEntity m WHERE m.organization = :organization ORDER BY m.createdAt"), - - @NamedQuery( - name = "searchOrganizationMembers", - query = "SELECT m FROM OrganizationMemberEntity m inner join UserEntity ue ON ue.id = m.userId" + - " WHERE m.organization = :organization" + - " AND (lower(ue.username) like lower(concat('%',:search,'%')) OR lower(ue.firstName) like lower(concat('%',:search,'%'))" + - " OR lower(ue.lastName) like lower(concat('%',:search,'%')) OR lower(ue.email) like lower(concat('%',:search,'%')))" + - " ORDER BY m.createdAt"), @NamedQuery( name = "getOrganizationMemberByUserId", query = diff --git a/src/main/java/io/phasetwo/service/representation/UserOrganizationMember.java b/src/main/java/io/phasetwo/service/representation/UserOrganizationMember.java new file mode 100644 index 00000000..b50033de --- /dev/null +++ b/src/main/java/io/phasetwo/service/representation/UserOrganizationMember.java @@ -0,0 +1,37 @@ +package io.phasetwo.service.representation; + +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.List; +import java.util.Map; + +public class UserOrganizationMember extends UserRepresentation { + + Map> organizationAttributes; + String organizationId; + List organizationRoles; + + public Map> getOrganizationAttributes() { + return organizationAttributes; + } + + public void setOrganizationAttributes(Map> organizationAttributes) { + this.organizationAttributes = organizationAttributes; + } + + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + + public List getOrganizationRoles() { + return organizationRoles; + } + + public void setOrganizationRoles(List organizationRoles) { + this.organizationRoles = organizationRoles; + } +} diff --git a/src/main/java/io/phasetwo/service/resource/Converters.java b/src/main/java/io/phasetwo/service/resource/Converters.java index 0086ae24..7788bbd9 100644 --- a/src/main/java/io/phasetwo/service/resource/Converters.java +++ b/src/main/java/io/phasetwo/service/resource/Converters.java @@ -9,87 +9,93 @@ import io.phasetwo.service.representation.Invitation; import io.phasetwo.service.representation.Organization; import io.phasetwo.service.representation.OrganizationRole; + import java.util.List; import java.util.Map; + import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.representations.account.UserRepresentation; -/** Utilities for converting Entities to/from Representations. */ +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; + +/** + * Utilities for converting Entities to/from Representations. + */ public class Converters { - public static OrganizationRole convertOrganizationRole(OrganizationRoleModel m) { - OrganizationRole r = - new OrganizationRole().id(m.getId()).name(m.getName()).description(m.getDescription()); - return r; - } + public static OrganizationRole convertOrganizationRole(OrganizationRoleModel m) { + OrganizationRole r = + new OrganizationRole().id(m.getId()).name(m.getName()).description(m.getDescription()); + return r; + } - public static Organization convertOrganizationModelToOrganization(OrganizationModel e) { - Organization o = - new Organization() - .id(e.getId()) - .name(e.getName()) - .displayName(e.getDisplayName()) - .domains(e.getDomains()) - .url(e.getUrl()) - .realm(e.getRealm().getName()); - o.setAttributes(e.getAttributes()); - return o; - } + public static Organization convertOrganizationModelToOrganization(OrganizationModel e) { + Organization o = + new Organization() + .id(e.getId()) + .name(e.getName()) + .displayName(e.getDisplayName()) + .domains(e.getDomains()) + .url(e.getUrl()) + .realm(e.getRealm().getName()); + o.setAttributes(e.getAttributes()); + return o; + } - public static UserRepresentation convertUserEntityToUserRepresentation(UserEntity e) { - UserRepresentation r = new UserRepresentation(); - r.setEmail(e.getEmail()); - r.setFirstName(e.getFirstName()); - r.setLastName(e.getLastName()); - r.setUsername(e.getUsername()); - r.setEmailVerified(e.isEmailVerified()); - r.setId(e.getId()); - Map> attr = Maps.newHashMap(); - e.getAttributes() - .forEach( - a -> { - List l = attr.get(a.getName()); - if (l == null) l = Lists.newArrayList(); - if (!l.contains(a.getValue())) l.add(a.getValue()); - attr.put(a.getName(), l); - }); - r.setAttributes(attr); - return r; - } + public static UserRepresentation convertUserEntityToUserRepresentation(UserEntity e) { + UserRepresentation r = new UserRepresentation(); + r.setEmail(e.getEmail()); + r.setFirstName(e.getFirstName()); + r.setLastName(e.getLastName()); + r.setUsername(e.getUsername()); + r.setEmailVerified(e.isEmailVerified()); + r.setId(e.getId()); + Map> attr = Maps.newHashMap(); + e.getAttributes() + .forEach( + a -> { + List l = attr.get(a.getName()); + if (l == null) l = Lists.newArrayList(); + if (!l.contains(a.getValue())) l.add(a.getValue()); + attr.put(a.getName(), l); + }); + r.setAttributes(attr); + return r; + } - public static Invitation convertInvitationEntityToInvitation(InvitationEntity e) { - Invitation i = - new Invitation() - .id(e.getId()) - .email(e.getEmail()) - .createdAt(e.getCreatedAt()) - .inviterId(e.getInviterId()) - .organizationId(e.getOrganization().getId()) - .roles(Lists.newArrayList(e.getRoles())); - Map> attr = Maps.newHashMap(); - e.getAttributes() - .forEach( - a -> { - List l = attr.get(a.getName()); - if (l == null) l = Lists.newArrayList(); - if (!l.contains(a.getValue())) l.add(a.getValue()); - attr.put(a.getName(), l); - }); - i.setAttributes(attr); - return i; - } + public static Invitation convertInvitationEntityToInvitation(InvitationEntity e) { + Invitation i = + new Invitation() + .id(e.getId()) + .email(e.getEmail()) + .createdAt(e.getCreatedAt()) + .inviterId(e.getInviterId()) + .organizationId(e.getOrganization().getId()) + .roles(Lists.newArrayList(e.getRoles())); + Map> attr = Maps.newHashMap(); + e.getAttributes() + .forEach( + a -> { + List l = attr.get(a.getName()); + if (l == null) l = Lists.newArrayList(); + if (!l.contains(a.getValue())) l.add(a.getValue()); + attr.put(a.getName(), l); + }); + i.setAttributes(attr); + return i; + } - public static Invitation convertInvitationModelToInvitation(InvitationModel e) { - Invitation i = - new Invitation() - .id(e.getId()) - .email(e.getEmail()) - .createdAt(e.getCreatedAt()) - .inviterId(e.getInviter().getId()) - .invitationUrl(e.getUrl()) - .organizationId(e.getOrganization().getId()) - .roles(Lists.newArrayList(e.getRoles())); - i.setAttributes(Maps.newHashMap(e.getAttributes())); - return i; - } + public static Invitation convertInvitationModelToInvitation(InvitationModel e) { + Invitation i = + new Invitation() + .id(e.getId()) + .email(e.getEmail()) + .createdAt(e.getCreatedAt()) + .inviterId(e.getInviter().getId()) + .invitationUrl(e.getUrl()) + .organizationId(e.getOrganization().getId()) + .roles(Lists.newArrayList(e.getRoles())); + i.setAttributes(Maps.newHashMap(e.getAttributes())); + return i; + } } diff --git a/src/main/java/io/phasetwo/service/resource/MembersResource.java b/src/main/java/io/phasetwo/service/resource/MembersResource.java index 667f6d91..0926ca27 100644 --- a/src/main/java/io/phasetwo/service/resource/MembersResource.java +++ b/src/main/java/io/phasetwo/service/resource/MembersResource.java @@ -3,7 +3,6 @@ import static io.phasetwo.service.Orgs.ACTIVE_ORGANIZATION; import static io.phasetwo.service.resource.OrganizationResourceType.*; import static org.keycloak.events.EventType.UPDATE_PROFILE; -import static org.keycloak.models.utils.ModelToRepresentation.*; import com.google.common.base.Strings; import io.phasetwo.service.model.OrganizationModel; @@ -42,10 +41,7 @@ public Stream getMembers( maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; return organization .searchForOrganizationMembersStream(searchQuery, firstResult, maxResults) - .map(m ->{ - var user = session.users().getUserById(realm, m.getUserId()); - return toRepresentation(session, realm, user); - }); + .map(m -> UserOrganizationMemberConverter.toRepresentation(session, realm, m)); } @GET diff --git a/src/main/java/io/phasetwo/service/resource/UserOrganizationMemberConverter.java b/src/main/java/io/phasetwo/service/resource/UserOrganizationMemberConverter.java new file mode 100644 index 00000000..fb5730ad --- /dev/null +++ b/src/main/java/io/phasetwo/service/resource/UserOrganizationMemberConverter.java @@ -0,0 +1,62 @@ +package io.phasetwo.service.resource; + +import io.phasetwo.service.model.OrganizationMemberModel; +import io.phasetwo.service.representation.UserOrganizationMember; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.OTPCredentialModel; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; + +public final class UserOrganizationMemberConverter { + + public static UserOrganizationMember toRepresentation(KeycloakSession session, + RealmModel realm, + OrganizationMemberModel organizationMemberModel + ) { + var rep = new UserOrganizationMember(); + + var user = session.users().getUserById(realm, organizationMemberModel.getUserId()); + rep.setId(user.getId()); + rep.setUsername(user.getUsername()); + rep.setCreatedTimestamp(user.getCreatedTimestamp()); + rep.setLastName(user.getLastName()); + rep.setFirstName(user.getFirstName()); + rep.setEmail(user.getEmail()); + rep.setEnabled(user.isEnabled()); + rep.setEmailVerified(user.isEmailVerified()); + rep.setTotp(user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE)); + rep.setDisableableCredentialTypes(user.credentialManager() + .getDisableableCredentialTypesStream().collect(Collectors.toSet())); + rep.setFederationLink(user.getFederationLink()); + rep.setNotBefore(isLightweightUser(user) ? user.getCreatedTimestamp().intValue() : session.users().getNotBeforeOfUser(realm, user)); + rep.setRequiredActions(user.getRequiredActionsStream().collect(Collectors.toList())); + + Map> attributes = user.getAttributes(); + Map> 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> attrs = new HashMap<>(copy); + rep.setAttributes(attrs); + } + + rep.setOrganizationAttributes(organizationMemberModel.getAttributes()); + rep.setOrganizationId(organizationMemberModel.getOrganization().getId()); + rep.setOrganizationRoles(organizationMemberModel.getRoles()); + + return rep; + } +}