Skip to content

Commit

Permalink
excludeAdminAccounts for members and roles resources (#296)
Browse files Browse the repository at this point in the history
* working/hacky excludeAdminAccounts for members and roles resources

* switched to UserEntity

* switched OrganizationMemberEntity to use UserEntity

* switched to userentity and updated named queries

* Fix queries for excluded org admin (#305)

* formatting

---------

Co-authored-by: rtufisi <[email protected]>
  • Loading branch information
xgp and rtufisi authored Jan 21, 2025
1 parent f05b725 commit cdcc2e2
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 94 deletions.
20 changes: 17 additions & 3 deletions src/main/java/io/phasetwo/service/model/OrganizationModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,25 @@ public interface OrganizationModel extends WithAttributes {

UserModel getCreatedBy();

Long getMembersCount();
default Long getMembersCount() {
return getMembersCount(false);
}

default Stream<UserModel> getMembersStream() {
return getMembersStream(false);
}

default Stream<UserModel> searchForMembersStream(
String search, Integer firstResult, Integer maxResults) {
return searchForMembersStream(search, firstResult, maxResults, false);
}

Long getMembersCount(boolean excludeAdminAccounts);

Stream<UserModel> getMembersStream();
Stream<UserModel> getMembersStream(boolean excludeAdminAccounts);

Stream<UserModel> searchForMembersStream(String search, Integer firstResult, Integer maxResults);
Stream<UserModel> searchForMembersStream(
String search, Integer firstResult, Integer maxResults, boolean excludeAdminAccounts);

boolean hasMembership(UserModel user);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ public interface OrganizationRoleModel {

void setDescription(String description);

Stream<UserModel> getUserMappingsStream();
default Stream<UserModel> getUserMappingsStream() {
return getUserMappingsStream(false);
}

Stream<UserModel> getUserMappingsStream(boolean excludeAdminAccounts);

void grantRole(UserModel user);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,13 @@ public static String createSearchString(String search) {
public Stream<OrganizationModel> getUserOrganizationsStream(RealmModel realm, UserModel user) {
TypedQuery<OrganizationMemberEntity> query =
em.createNamedQuery("getOrganizationMembershipsByUserId", OrganizationMemberEntity.class);
query.setParameter("id", user.getId());
query.setParameter("userId", user.getId());
return query
.getResultStream()
.map(e -> new OrganizationAdapter(session, realm, em, e.getOrganization()));
}

@Override
@SuppressWarnings("unchecked")
public Stream<OrganizationModel> searchForOrganizationStream(
RealmModel realm,
Map<String, String> attributes,
Expand Down Expand Up @@ -249,23 +248,20 @@ private List<Predicate> attributePredicates(
continue;
}

switch (key) {
case "name":
predicates.add(
builder.or(
builder.like(builder.lower(root.get("name")), "%" + value.toLowerCase() + "%"),
builder.like(
builder.lower(root.get("displayName")), "%" + value.toLowerCase() + "%")));
break;
default:
Join<ExtOrganizationEntity, OrganizationAttributeEntity> 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 (key.equals("name")) {
predicates.add(
builder.or(
builder.like(builder.lower(root.get("name")), "%" + value.toLowerCase() + "%"),
builder.like(
builder.lower(root.get("displayName")), "%" + value.toLowerCase() + "%")));
} else {
Join<ExtOrganizationEntity, OrganizationAttributeEntity> 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())));
}
}

Expand All @@ -282,6 +278,6 @@ private Predicate memberPredicate(UserModel member, Root<ExtOrganizationEntity>
Join<ExtOrganizationEntity, OrganizationMemberEntity> membersJoin =
root.join("members", JoinType.LEFT);

return builder.equal(membersJoin.get("userId"), member.getId());
return builder.equal(membersJoin.get("user").get("id"), member.getId());
}
}
136 changes: 104 additions & 32 deletions src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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;

import com.google.common.base.Strings;
import io.phasetwo.service.model.DomainModel;
import io.phasetwo.service.model.InvitationModel;
import io.phasetwo.service.model.OrganizationModel;
Expand All @@ -17,8 +22,15 @@
import io.phasetwo.service.util.IdentityProviders;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
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 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;
Expand All @@ -28,6 +40,8 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.jpa.UserAdapter;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils;

public class OrganizationAdapter implements OrganizationModel, JpaModel<ExtOrganizationEntity> {
Expand All @@ -37,6 +51,8 @@ public class OrganizationAdapter implements OrganizationModel, JpaModel<ExtOrgan
protected final EntityManager em;
protected final RealmModel realm;

private static final char ESCAPE_BACKSLASH = '\\';

public OrganizationAdapter(
KeycloakSession session, RealmModel realm, EntityManager em, ExtOrganizationEntity org) {
this.session = session;
Expand Down Expand Up @@ -164,43 +180,89 @@ public void setAttribute(String name, List<String> values) {
}
}

private TypedQuery<OrganizationMemberEntity> membersQuery(String search, boolean excludeAdmin) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<OrganizationMemberEntity> criteriaQuery =
cb.createQuery(OrganizationMemberEntity.class);

Root<OrganizationMemberEntity> root = criteriaQuery.from(OrganizationMemberEntity.class);

List<Predicate> predicates = new ArrayList<>();
// defining the organization search clause
predicates.add(cb.equal(root.get("organization"), org));

// join tables
From<OrganizationMemberEntity, UserEntity> userJoin = root.join("user");
if (search != null && !search.isEmpty()) {
List<Predicate> searchTermsPredicates = new ArrayList<>();
// define search terms
for (String stringToSearch : search.trim().split(",")) {
searchTermsPredicates.add(
cb.or(getSearchOptionPredicateArray(stringToSearch, cb, userJoin)));
}
predicates.add(cb.or(searchTermsPredicates.toArray(Predicate[]::new)));
}

if (excludeAdmin) {
List<Predicate> excludeAdminsPredicates = new ArrayList<>();
excludeAdminsPredicates.add(cb.like(userJoin.get(USERNAME), "org-admin-%", ESCAPE_BACKSLASH));
excludeAdminsPredicates.add(cb.equal(cb.length(userJoin.get(USERNAME)), "46"));

predicates.add(cb.not(cb.and(excludeAdminsPredicates.toArray(Predicate[]::new))));
}

criteriaQuery
.where(predicates.toArray(Predicate[]::new))
.orderBy(cb.asc(root.get("createdAt")));
return em.createQuery(criteriaQuery);
}

private Predicate[] getSearchOptionPredicateArray(
String value, CriteriaBuilder builder, From<?, UserEntity> from) {
value = value.trim().toLowerCase();
List<Predicate> 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);
}

@Override
public Stream<UserModel> searchForMembersStream(
String search, Integer firstResult, Integer maxResults) {
String[] searchTerms = Strings.isNullOrEmpty(search) ? new String[0] : search.split(",");
// TODO this could be optimized for large member lists with a query
return getMembersStream()
.filter(
(m) -> {
for (String searchTerm : searchTerms) {
String term = searchTerm.trim().toLowerCase();
if (term.isEmpty()) continue;
if ((m.getEmail() != null && m.getEmail().toLowerCase().contains(term))
|| (m.getUsername() != null && m.getUsername().toLowerCase().contains(term))
|| (m.getFirstName() != null && m.getFirstName().toLowerCase().contains(term))
|| (m.getLastName() != null && m.getLastName().toLowerCase().contains(term))) {
return true;
}
}
return searchTerms.length == 0;
})
.skip(firstResult)
.limit(maxResults);
}

@Override
public Long getMembersCount() {
TypedQuery<Long> query = em.createNamedQuery("getOrganizationMembersCount", Long.class);
String search, Integer firstResult, Integer maxResults, boolean excludeAdmin) {
var query = membersQuery(search, excludeAdmin);
return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
.filter(Objects::nonNull)
.map(OrganizationMemberEntity::getUserId)
.map(userId -> session.users().getUserById(realm, userId))
.filter(u -> u.getServiceAccountClientLink() == null);
}

@Override
public Long getMembersCount(boolean excludeAdmin) {
TypedQuery<Long> query =
em.createNamedQuery(
excludeAdmin
? "getOrganizationMembersCountExcludeAdmin"
: "getOrganizationMembersCount",
Long.class);
query.setParameter("organization", org);
return query.getSingleResult();
}

@Override
public Stream<UserModel> getMembersStream() {
return org.getMembers().stream()
public Stream<UserModel> getMembersStream(boolean excludeAdmin) {
var query = membersQuery(null, excludeAdmin);
return query
.getResultStream()
.filter(Objects::nonNull)
.map(OrganizationMemberEntity::getUserId)
.map(uid -> session.users().getUserById(realm, uid))
.filter(u -> u != null && u.getServiceAccountClientLink() == null);
.map(userId -> session.users().getUserById(realm, userId))
.filter(u -> u.getServiceAccountClientLink() == null);
}

@Override
Expand All @@ -213,12 +275,21 @@ public void grantMembership(UserModel user) {
if (hasMembership(user)) return;
OrganizationMemberEntity m = new OrganizationMemberEntity();
m.setId(KeycloakModelUtils.generateId());
m.setUserId(user.getId());
UserEntity u = entityFromModel(user);
m.setUser(u);
m.setOrganization(org);
em.persist(m);
org.getMembers().add(m);
}

private UserEntity entityFromModel(UserModel user) {
if (user instanceof UserAdapter) {
return ((UserAdapter) user).getEntity();
} else {
return em.find(UserEntity.class, user.getId());
}
}

@Override
public void revokeMembership(UserModel user) {
if (!hasMembership(user)) return;
Expand Down Expand Up @@ -290,7 +361,8 @@ public Stream<OrganizationRoleModel> getRolesStream() {
public Stream<OrganizationRoleModel> getRolesByUserStream(UserModel user) {
TypedQuery<UserOrganizationRoleMappingEntity> query =
em.createNamedQuery("getMappingsByUser", UserOrganizationRoleMappingEntity.class);
query.setParameter("userId", user.getId());
UserEntity u = entityFromModel(user);
query.setParameter("user", u);
query.setParameter("orgId", org.getId());
try {
return query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.jpa.UserAdapter;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils;

public class OrganizationRoleAdapter
Expand Down Expand Up @@ -66,10 +68,17 @@ public void setDescription(String description) {
}

@Override
public Stream<UserModel> getUserMappingsStream() {
return role.getUserMappings().stream()
.map(m -> m.getUserId())
.map(uid -> session.users().getUserById(realm, uid));
public Stream<UserModel> getUserMappingsStream(boolean excludeAdmin) {
TypedQuery<UserOrganizationRoleMappingEntity> query =
em.createNamedQuery(
excludeAdmin ? "getMappingByRoleExcludeAdmin" : "getMappingByRole",
UserOrganizationRoleMappingEntity.class);
query.setParameter("role", role);
return query
.getResultStream()
.map(UserOrganizationRoleMappingEntity::getUserId)
.map(uid -> session.users().getUserById(realm, uid))
.filter(u -> u.getServiceAccountClientLink() == null);
}

@Override
Expand All @@ -80,7 +89,11 @@ public void grantRole(UserModel user) {
if (hasRole(user)) return;
UserOrganizationRoleMappingEntity m = new UserOrganizationRoleMappingEntity();
m.setId(KeycloakModelUtils.generateId());
m.setUserId(user.getId());
if (user instanceof UserAdapter) {
m.setUser(((UserAdapter) user).getEntity());
} else {
m.setUser(em.find(UserEntity.class, user.getId()));
}
m.setRole(role);
em.persist(m);
role.getUserMappings().add(m);
Expand Down
Loading

0 comments on commit cdcc2e2

Please sign in to comment.