Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 287 Add attributes to an OrganizationMembership #290

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.phasetwo.service.model;

import java.util.List;

public interface OrganizationMemberModel extends WithAttributes {

String getId();

String getUserId();

OrganizationModel getOrganization();

List<String> getRoles();
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public interface OrganizationModel extends WithAttributes {

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

Stream<OrganizationMemberModel> getOrganizationMembersStream();

Stream<OrganizationMemberModel> searchForOrganizationMembersStream(
String search, Integer firstResult, Integer maxResults);

boolean hasMembership(UserModel user);

void grantMembership(UserModel user);
Expand Down Expand Up @@ -74,6 +79,8 @@ default OrganizationRoleModel getRoleByName(String name) {
.orElse(null);
}

OrganizationMemberModel getMembershipDetails(UserModel user);

void removeRole(String name);

OrganizationRoleModel addRole(String name);
Expand Down
110 changes: 109 additions & 1 deletion src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
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.OrganizationMemberModel;
import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.OrganizationRoleModel;
import io.phasetwo.service.model.jpa.entity.DomainEntity;
Expand All @@ -17,8 +24,16 @@
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 jakarta.persistence.criteria.Subquery;
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 +43,7 @@
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<ExtOrganizationEntity> {
Expand All @@ -37,6 +53,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 @@ -188,6 +206,72 @@ public Stream<UserModel> searchForMembersStream(
.limit(maxResults);
}

@Override
public Stream<OrganizationMemberModel> getOrganizationMembersStream() {
TypedQuery<OrganizationMemberEntity> query =
em.createNamedQuery("getOrganizationMembers", OrganizationMemberEntity.class);
query.setParameter("organization", org);

return query
.getResultStream()
.map(
organizationMemberEntity ->
new OrganizationMemberAdapter(session, realm, em, organizationMemberEntity));
}

@Override
public Stream<OrganizationMemberModel> searchForOrganizationMembersStream(
String search, Integer firstResult, Integer maxResults) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<OrganizationMemberEntity> criteriaQuery =
criteriaBuilder.createQuery(OrganizationMemberEntity.class);

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

List<Predicate> 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<OrganizationMemberEntity> query = em.createQuery(criteriaQuery);

return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
.filter(Objects::nonNull)
.map(
organizationMemberEntity ->
new OrganizationMemberAdapter(session, realm, em, organizationMemberEntity));
}

private Subquery<String> userIdsSubquery(CriteriaQuery<?> query, String search) {
CriteriaBuilder cb = em.getCriteriaBuilder();
Subquery<String> subquery = query.subquery(String.class);
Root<UserEntity> subRoot = subquery.from(UserEntity.class);

subquery.select(subRoot.get("id"));
List<Predicate> subqueryPredicates = new ArrayList<>();

subqueryPredicates.add(cb.equal(subRoot.get("realmId"), realm.getId()));

List<Predicate> 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<Long> query = em.createNamedQuery("getOrganizationMembersCount", Long.class);
Expand Down Expand Up @@ -301,6 +385,15 @@ public Stream<OrganizationRoleModel> getRolesByUserStream(UserModel user) {
}
}

@Override
public OrganizationMemberModel getMembershipDetails(UserModel user) {
TypedQuery<OrganizationMemberModel> query =
em.createNamedQuery("getOrganizationMemberByUserId", OrganizationMemberModel.class);
query.setParameter("organization", org);
query.setParameter("id", user.getId());
return query.getSingleResult();
}

@Override
public void removeRole(String name) {
org.getRoles().removeIf(r -> r.getName().equals(name));
Expand All @@ -319,7 +412,8 @@ public OrganizationRoleModel addRole(String name) {

@Override
public Stream<IdentityProviderModel> getIdentityProvidersStream() {
return session.identityProviders()
return session
.identityProviders()
.getAllStream()
// Todo: do we need to apply here a role filter? I believe not since its part of the
// HomeIdpDiscoverer
Expand All @@ -330,4 +424,18 @@ public Stream<IdentityProviderModel> getIdentityProvidersStream() {
return orgs.contains(getId());
});
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.phasetwo.service.model.jpa;

import io.phasetwo.service.model.OrganizationMemberModel;
import io.phasetwo.service.model.OrganizationModel;
import io.phasetwo.service.model.jpa.entity.OrganizationMemberAttributeEntity;
import io.phasetwo.service.model.jpa.entity.OrganizationMemberEntity;
import io.phasetwo.service.model.jpa.entity.OrganizationRoleEntity;
import io.phasetwo.service.model.jpa.entity.UserOrganizationRoleMappingEntity;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Map;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.utils.KeycloakModelUtils;

public class OrganizationMemberAdapter
implements OrganizationMemberModel, JpaModel<OrganizationMemberEntity> {

protected final KeycloakSession session;
protected final OrganizationMemberEntity organizationMemberEntity;
protected final EntityManager em;
protected final RealmModel realm;

public OrganizationMemberAdapter(
KeycloakSession session,
RealmModel realm,
EntityManager em,
OrganizationMemberEntity organizationMemberEntity) {
this.session = session;
this.em = em;
this.organizationMemberEntity = organizationMemberEntity;
this.realm = realm;
}

@Override
public OrganizationMemberEntity getEntity() {
return organizationMemberEntity;
}

@Override
public String getId() {
return organizationMemberEntity.getId();
}

@Override
public String getUserId() {
return organizationMemberEntity.getUserId();
}

@Override
public OrganizationModel getOrganization() {
return new OrganizationAdapter(session, realm, em, organizationMemberEntity.getOrganization());
}

@Override
public List<String> getRoles() {
return organizationMemberEntity.getOrganization().getRoles().stream()
.flatMap(organizationRoleEntity -> organizationRoleEntity.getUserMappings().stream())
.filter(
userOrganizationRoleMappingEntity ->
userOrganizationRoleMappingEntity
.getUserId()
.equals(organizationMemberEntity.getUserId()))
.map(UserOrganizationRoleMappingEntity::getRole)
.map(OrganizationRoleEntity::getName)
.toList();
}

@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
for (OrganizationMemberAttributeEntity attr : organizationMemberEntity.getAttributes()) {
result.add(attr.getName(), attr.getValue());
}
return result;
}

@Override
public void removeAttribute(String name) {
organizationMemberEntity
.getAttributes()
.removeIf(attribute -> attribute.getName().equals(name));
}

@Override
public void removeAttributes() {
organizationMemberEntity.getAttributes().clear();
}

@Override
public void setAttribute(String name, List<String> values) {
removeAttribute(name);
for (String value : values) {
OrganizationMemberAttributeEntity a = new OrganizationMemberAttributeEntity();
a.setId(KeycloakModelUtils.generateId());
a.setName(name);
a.setValue(value);
a.setOrganizationMember(organizationMemberEntity);
em.persist(a);
organizationMemberEntity.getAttributes().add(a);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class OrganizationEntityProvider implements JpaEntityProvider {
ExtOrganizationEntity.class,
OrganizationAttributeEntity.class,
OrganizationMemberEntity.class,
OrganizationMemberAttributeEntity.class,
OrganizationRoleEntity.class,
UserOrganizationRoleMappingEntity.class,
InvitationEntity.class,
Expand Down
Loading