From 05c159ed280544fb9d85ddfe73f0bdc28c73f28e Mon Sep 17 00:00:00 2001 From: razvantufisi Date: Sat, 28 Dec 2024 18:11:48 +0200 Subject: [PATCH] #287 Add attributes to an OrganizationMembership --- .../model/OrganizationMembershipModel.java | 10 +++ .../service/model/OrganizationModel.java | 6 ++ .../model/jpa/OrganizationAdapter.java | 24 +++++ .../jpa/OrganizationMembershipAdapter.java | 87 +++++++++++++++++++ .../entity/OrganizationEntityProvider.java | 1 + .../jpa/entity/OrganizationMemberEntity.java | 16 ++++ ...OrganizationMembershipAttributeEntity.java | 84 ++++++++++++++++++ .../service/resource/MembersResource.java | 5 +- .../jpa-changelog-phasetwo-20241228.xml | 17 ++++ .../jpa-changelog-phasetwo-master.xml | 1 + 10 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/phasetwo/service/model/OrganizationMembershipModel.java create mode 100644 src/main/java/io/phasetwo/service/model/jpa/OrganizationMembershipAdapter.java create mode 100644 src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMembershipAttributeEntity.java create mode 100644 src/main/resources/META-INF/jpa-changelog-phasetwo-20241228.xml diff --git a/src/main/java/io/phasetwo/service/model/OrganizationMembershipModel.java b/src/main/java/io/phasetwo/service/model/OrganizationMembershipModel.java new file mode 100644 index 00000000..d3fc393f --- /dev/null +++ b/src/main/java/io/phasetwo/service/model/OrganizationMembershipModel.java @@ -0,0 +1,10 @@ +package io.phasetwo.service.model; + +public interface OrganizationMembershipModel extends WithAttributes { + + String getId(); + + String getUserId(); + + OrganizationModel getOrganization(); +} diff --git a/src/main/java/io/phasetwo/service/model/OrganizationModel.java b/src/main/java/io/phasetwo/service/model/OrganizationModel.java index abf966ea..511cacc9 100644 --- a/src/main/java/io/phasetwo/service/model/OrganizationModel.java +++ b/src/main/java/io/phasetwo/service/model/OrganizationModel.java @@ -41,6 +41,10 @@ public interface OrganizationModel extends WithAttributes { Stream searchForMembersStream(String search, Integer firstResult, Integer maxResults); + Stream getOrganizationMembersStream(); + + Stream searchForOrganizationMembersStream(String search, Integer firstResult, Integer maxResults); + boolean hasMembership(UserModel user); void grantMembership(UserModel user); @@ -74,6 +78,8 @@ default OrganizationRoleModel getRoleByName(String name) { .orElse(null); } + OrganizationMembershipModel getMembershipDetails(UserModel user); + void removeRole(String name); OrganizationRoleModel addRole(String name); 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 767e546b..d2c93ee3 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java +++ b/src/main/java/io/phasetwo/service/model/jpa/OrganizationAdapter.java @@ -5,6 +5,7 @@ import com.google.common.base.Strings; import io.phasetwo.service.model.DomainModel; import io.phasetwo.service.model.InvitationModel; +import io.phasetwo.service.model.OrganizationMembershipModel; import io.phasetwo.service.model.OrganizationModel; import io.phasetwo.service.model.OrganizationRoleModel; import io.phasetwo.service.model.jpa.entity.DomainEntity; @@ -12,6 +13,7 @@ import io.phasetwo.service.model.jpa.entity.InvitationEntity; import io.phasetwo.service.model.jpa.entity.OrganizationAttributeEntity; import io.phasetwo.service.model.jpa.entity.OrganizationMemberEntity; +import io.phasetwo.service.model.jpa.entity.OrganizationMembershipAttributeEntity; import io.phasetwo.service.model.jpa.entity.OrganizationRoleEntity; import io.phasetwo.service.model.jpa.entity.UserOrganizationRoleMappingEntity; import io.phasetwo.service.util.IdentityProviders; @@ -188,6 +190,20 @@ public Stream searchForMembersStream( .limit(maxResults); } + @Override + public Stream getOrganizationMembersStream() { + TypedQuery query = em.createNamedQuery("getOrganizationMembers", OrganizationMemberEntity.class); + query.setParameter("organization", org); + + return query.getResultStream() + .map(organizationMemberEntity -> new OrganizationMembershipAdapter(session, realm, em, organizationMemberEntity)); + } + + @Override + public Stream searchForOrganizationMembersStream(String search, Integer firstResult, Integer maxResults) { + return Stream.empty(); + } + @Override public Long getMembersCount() { TypedQuery query = em.createNamedQuery("getOrganizationMembersCount", Long.class); @@ -301,6 +317,14 @@ public Stream getRolesByUserStream(UserModel user) { } } + @Override + public OrganizationMembershipModel getMembershipDetails(UserModel user) { + TypedQuery query = em.createNamedQuery("getOrganizationMemberByUserId", OrganizationMembershipModel.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)); diff --git a/src/main/java/io/phasetwo/service/model/jpa/OrganizationMembershipAdapter.java b/src/main/java/io/phasetwo/service/model/jpa/OrganizationMembershipAdapter.java new file mode 100644 index 00000000..29f97e49 --- /dev/null +++ b/src/main/java/io/phasetwo/service/model/jpa/OrganizationMembershipAdapter.java @@ -0,0 +1,87 @@ +package io.phasetwo.service.model.jpa; + +import io.phasetwo.service.model.OrganizationMembershipModel; +import io.phasetwo.service.model.OrganizationModel; +import io.phasetwo.service.model.OrganizationProvider; +import io.phasetwo.service.model.jpa.entity.OrganizationMemberEntity; +import io.phasetwo.service.model.jpa.entity.OrganizationMembershipAttributeEntity; +import jakarta.persistence.EntityManager; +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; + +import java.util.List; +import java.util.Map; + +public class OrganizationMembershipAdapter implements OrganizationMembershipModel, JpaModel { + + protected final KeycloakSession session; + protected final OrganizationMemberEntity organizationMemberEntity; + protected final EntityManager em; + protected final RealmModel realm; + + public OrganizationMembershipAdapter( + 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 session + .getProvider(OrganizationProvider.class) + .getOrganizationById(realm, organizationMemberEntity.getOrganization().getId()); + } + + @Override + public Map> getAttributes() { + MultivaluedHashMap result = new MultivaluedHashMap<>(); + for (OrganizationMembershipAttributeEntity 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 values) { + removeAttribute(name); + for (String value : values) { + OrganizationMembershipAttributeEntity a = new OrganizationMembershipAttributeEntity(); + a.setId(KeycloakModelUtils.generateId()); + a.setName(name); + a.setValue(value); + a.setOrganizationMember(organizationMemberEntity); + em.persist(a); + organizationMemberEntity.getAttributes().add(a); + } + } +} diff --git a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationEntityProvider.java b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationEntityProvider.java index 61e1863c..85e2307d 100644 --- a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationEntityProvider.java +++ b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationEntityProvider.java @@ -12,6 +12,7 @@ public class OrganizationEntityProvider implements JpaEntityProvider { ExtOrganizationEntity.class, OrganizationAttributeEntity.class, OrganizationMemberEntity.class, + OrganizationMembershipAttributeEntity.class, OrganizationRoleEntity.class, UserOrganizationRoleMappingEntity.class, InvitationEntity.class, 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 fd2fd9ee..0950ba24 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 @@ -2,6 +2,7 @@ import jakarta.persistence.Access; import jakarta.persistence.AccessType; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -10,11 +11,15 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; +import jakarta.persistence.OneToMany; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; import jakarta.persistence.UniqueConstraint; + +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.Objects; @@ -60,6 +65,9 @@ public class OrganizationMemberEntity { @Column(name = "CREATED_AT") protected Date createdAt; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "organizationMember") + protected Collection attributes = new ArrayList<>(); + @PrePersist protected void onCreate() { if (createdAt == null) createdAt = new Date(); @@ -97,6 +105,14 @@ public void setCreatedAt(Date at) { createdAt = at; } + public Collection getAttributes() { + return attributes; + } + + public void setAttributes(Collection attributes) { + this.attributes = attributes; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMembershipAttributeEntity.java b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMembershipAttributeEntity.java new file mode 100644 index 00000000..401ff267 --- /dev/null +++ b/src/main/java/io/phasetwo/service/model/jpa/entity/OrganizationMembershipAttributeEntity.java @@ -0,0 +1,84 @@ +package io.phasetwo.service.model.jpa.entity; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import org.hibernate.annotations.Nationalized; + +import java.util.Objects; + +@Table( + name = "ORGANIZATION_MEMBER_ATTRIBUTE", + uniqueConstraints = {@UniqueConstraint(columnNames = {"ORGANIZATION_MEMBER_ID", "NAME"})}) +@Entity +public class OrganizationMembershipAttributeEntity { + + @Id + @Column(name = "ID", length = 36) + @Access( + AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This + // avoids an extra SQL + protected String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ORGANIZATION_MEMBER_ID") + protected OrganizationMemberEntity organizationMember; + + @Column(name = "NAME") + protected String name; + + @Nationalized + @Column(name = "VALUE") + protected String value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OrganizationMemberEntity getOrganizationMember() { + return organizationMember; + } + + public void setOrganizationMember(OrganizationMemberEntity organizationMember) { + this.organizationMember = organizationMember; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + OrganizationMembershipAttributeEntity that = (OrganizationMembershipAttributeEntity) o; + return Objects.equals(id, that.id) && Objects.equals(organizationMember, that.organizationMember); + } + + @Override + public int hashCode() { + return Objects.hash(id, organizationMember); + } +} diff --git a/src/main/java/io/phasetwo/service/resource/MembersResource.java b/src/main/java/io/phasetwo/service/resource/MembersResource.java index 1bc6a3a2..6c1e548b 100644 --- a/src/main/java/io/phasetwo/service/resource/MembersResource.java +++ b/src/main/java/io/phasetwo/service/resource/MembersResource.java @@ -42,7 +42,10 @@ public Stream getMembers( maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; return organization .searchForMembersStream(searchQuery, firstResult, maxResults) - .map(m -> toRepresentation(session, realm, m)); + .map(m ->{ + var membership = organization.getOrganizationMembersStream(); + return toRepresentation(session, realm, m); + }); } @GET diff --git a/src/main/resources/META-INF/jpa-changelog-phasetwo-20241228.xml b/src/main/resources/META-INF/jpa-changelog-phasetwo-20241228.xml new file mode 100644 index 00000000..464a8c07 --- /dev/null +++ b/src/main/resources/META-INF/jpa-changelog-phasetwo-20241228.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/jpa-changelog-phasetwo-master.xml b/src/main/resources/META-INF/jpa-changelog-phasetwo-master.xml index 864a6edb..a78db974 100644 --- a/src/main/resources/META-INF/jpa-changelog-phasetwo-master.xml +++ b/src/main/resources/META-INF/jpa-changelog-phasetwo-master.xml @@ -24,5 +24,6 @@ +