package edu.harvard.iq.dataverse.authorization.groups.impl.explicit; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.groups.Group; import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.authorization.groups.GroupException; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.PostLoad; import javax.persistence.PrePersist; import javax.persistence.Table; import javax.persistence.Transient; import javax.validation.constraints.Pattern; import org.hibernate.validator.constraints.NotBlank; /** * A group that explicitly lists {@link RoleAssignee}s that belong to it. Implementation-wise, * there are three cases here: {@link AuthenticatedUser}s, other {@link ExplicitGroup}s, and all the rest. * AuthenticatedUsers and ExplicitGroups go in tables of their own. The rest are kept via their identifier. * * @author michael */ @NamedQueries({ @NamedQuery( name="ExplicitGroup.findAll", query="SELECT eg FROM ExplicitGroup eg"), @NamedQuery( name="ExplicitGroup.findByOwnerIdAndAlias", query="SELECT eg FROM ExplicitGroup eg WHERE eg.owner.id=:ownerId AND eg.groupAliasInOwner=:alias"), @NamedQuery( name="ExplicitGroup.findByAlias", query="SELECT eg FROM ExplicitGroup eg WHERE eg.groupAlias=:alias"), @NamedQuery( name="ExplicitGroup.findByOwnerId", query="SELECT eg FROM ExplicitGroup eg WHERE eg.owner.id=:ownerId"), @NamedQuery( name="ExplicitGroup.findByOwnerAndAuthUserId", query="SELECT eg FROM ExplicitGroup eg join eg.containedAuthenticatedUsers au " +"WHERE eg.owner.id=:ownerId AND au.id=:authUserId"), @NamedQuery( name="ExplicitGroup.findByOwnerAndSubExGroupId", query="SELECT eg FROM ExplicitGroup eg join eg.containedExplicitGroups ceg " +"WHERE eg.owner.id=:ownerId AND ceg.id=:subExGroupId"), @NamedQuery( name="ExplicitGroup.findByOwnerAndRAIdtf", query="SELECT eg FROM ExplicitGroup eg join eg.containedRoleAssignees ra " +"WHERE eg.owner.id=:ownerId AND ra=:raIdtf"), @NamedQuery( name="ExplicitGroup.findByAuthenticatedUserIdentifier", query="SELECT eg FROM ExplicitGroup eg JOIN eg.containedAuthenticatedUsers au " + "WHERE au.userIdentifier=:authenticatedUserIdentifier"), @NamedQuery( name="ExplicitGroup.findByRoleAssgineeIdentifier", query="SELECT eg FROM ExplicitGroup eg JOIN eg.containedRoleAssignees cra " + "WHERE cra=:roleAssigneeIdentifier"), @NamedQuery( name="ExplicitGroup.findByContainedExplicitGroupId", query="SELECT eg FROM ExplicitGroup eg join eg.containedExplicitGroups ceg " +"WHERE ceg.id=:containedExplicitGroupId") }) @Entity @Table(indexes = {@Index(columnList="owner_id"), @Index(columnList="groupaliasinowner")}) public class ExplicitGroup implements Group, java.io.Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; /** * Authenticated users directly added to the group. */ @ManyToMany private Set<AuthenticatedUser> containedAuthenticatedUsers; /** * Explicit groups that belong to {@code this} explicit gorups. */ @ManyToMany @JoinTable(name = "explicitgroup_explicitgroup", joinColumns = @JoinColumn(name="explicitgroup_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name="containedexplicitgroups_id", referencedColumnName = "id") ) Set<ExplicitGroup> containedExplicitGroups; /** * All the role assignees that belong to this group * and are not {@link authenticatedUser}s or {@ExplicitGroup}s, are stored * here via their identifiers. * * @see RoleAssignee#getIdentifier() */ @ElementCollection private Set<String> containedRoleAssignees; @Column( length = 1024 ) private String description; @NotBlank private String displayName; /** * The DvObject under which this group is defined. */ @ManyToOne DvObject owner; /** Given alias of the group, e.g by the user that created it. Unique in the owner. */ @NotBlank @Pattern(regexp = "[a-zA-Z0-9\\_\\-]*", message = "Found an illegal character(s). Valid characters are a-Z, 0-9, '_', and '-'.") private String groupAliasInOwner; /** Alias of the group. Calculated from the group's name and its owner id. Unique in the table. */ @Column( unique = true ) private String groupAlias; @Transient private ExplicitGroupProvider provider; public ExplicitGroup( ExplicitGroupProvider prv ) { provider = prv; containedAuthenticatedUsers = new HashSet<>(); containedExplicitGroups = new HashSet<>(); containedRoleAssignees = new TreeSet<>(); } public Set<AuthenticatedUser> getContainedAuthenticatedUsers() { return containedAuthenticatedUsers; } public Set<ExplicitGroup> getContainedExplicitGroups() { return containedExplicitGroups; } /** * Constructor for JPA. */ protected ExplicitGroup() {} public void add( User u ) { if ( u instanceof AuthenticatedUser ) { containedAuthenticatedUsers.add((AuthenticatedUser)u); } else { containedRoleAssignees.add( u.getIdentifier() ); } } /** * Adds the {@link RoleAssignee} to {@code this} group. * * @param ra the role assignee to be added to this group. * @throws GroupException if {@code ra} is a group, and is either an ancestor of {@code this}, * or is defined in a dataverse that is not an ancestor of {@code this.owner}. */ public void add( RoleAssignee ra ) throws GroupException { if ( ra.equals(this) ) { throw new GroupException(this, "A group cannot be added to itself."); } if ( ra instanceof User ) { add( (User)ra ); } else { if ( ra instanceof ExplicitGroup ) { // validate no circular deps ExplicitGroup g = (ExplicitGroup) ra; if ( g.structuralContains(this) ) { throw new GroupException(this, "A group cannot be added to one of its childs."); } if ( g.owner.isAncestorOf(owner) ) { containedExplicitGroups.add( g ); } else { throw new GroupException(this, "Cannot add " + g + ", as it is not defined in " + owner + " or one of its ancestors."); } } else { containedRoleAssignees.add( ra.getIdentifier() ); } } } public void remove(RoleAssignee roleAssignee) { removeByRoleAssgineeIdentifier( roleAssignee.getIdentifier() ); } /** * Returns all the role assignee identifiers in this group. <br> * <b>Note</b> some of the identifiers may be stale (i.e. group deleted but * identifiers lingered for a while). * * @return A list of the role assignee identifiers. */ public Set<String> getContainedRoleAssgineeIdentifiers() { Set<String> retVal = new TreeSet<>(); retVal.addAll( containedRoleAssignees ); for ( ExplicitGroup subg : containedExplicitGroups ) { retVal.add( subg.getIdentifier() ); } for ( AuthenticatedUser au : containedAuthenticatedUsers ) { retVal.add( au.getIdentifier() ); } return retVal; } public void removeByRoleAssgineeIdentifier( String idtf ) { if ( containedRoleAssignees.contains(idtf) ) { containedRoleAssignees.remove(idtf); } else { for ( AuthenticatedUser au : containedAuthenticatedUsers ) { if ( au.getIdentifier().equals(idtf) ) { containedAuthenticatedUsers.remove(au); return; } } for ( ExplicitGroup eg : containedExplicitGroups ) { if ( eg.getIdentifier().equals(idtf) ) { containedExplicitGroups.remove(eg); return; } } } } /** * Returns a set of all direct members of the group, including * logical role assignees. * @return members of the group. */ public Set<RoleAssignee> getDirectMembers() { Set<RoleAssignee> res = new HashSet<>(); res.addAll( containedExplicitGroups ); res.addAll( containedAuthenticatedUsers ); for ( String idtf : containedRoleAssignees ) { RoleAssignee ra = provider.findRoleAssignee(idtf); if ( ra != null ) { res.add(ra); } } return res; } @Override public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public boolean contains(DataverseRequest req) { return containsDirectly(req) || containsIndirectly(req); } /** * Looks at structural containment: whether {@code ra} is part of the * group's structure. It mostly the same as {@link #contains(edu.harvard.iq.dataverse.engine.command.DataverseRequest)}, * except for logical containment. So if an ExplicitGroup contains {@link AuthenticatedUsers} but not * a specific {@link AuthenticatedUser} {@code u}, {@code structuralContains(u)} * would return {@code false} while {@code contains( request(u, ...) )} would return true; * * @param ra * @return {@code true} iff the role assignee is structurally a part of the group. */ public boolean structuralContains(RoleAssignee ra) { // direct containment if ( ra instanceof AuthenticatedUser ) { if ( containedAuthenticatedUsers.contains((AuthenticatedUser)ra) ) { return true; } } else if ( ra instanceof ExplicitGroup ) { if ( containedExplicitGroups.contains((ExplicitGroup)ra) ) { return true; } } else { if ( containedRoleAssignees.contains(ra.getIdentifier()) ) { return true; } } // no direct containment. Recurse. for ( ExplicitGroup eg: containedExplicitGroups ) { if ( eg.structuralContains(ra) ) { return true; } } return false; } /** * @param req * @return {@code true} iff the request is contained in the group or in an included non-explicit group. */ protected boolean containsDirectly( DataverseRequest req ) { User ra = req.getUser(); if ( ra instanceof AuthenticatedUser ) { AuthenticatedUser au = (AuthenticatedUser) ra; if ( containedAuthenticatedUsers.contains(au) ) { return true; } } if ( containedRoleAssignees.contains(ra.getIdentifier()) ) { return true; } for ( String craIdtf : containedRoleAssignees ) { // Need to retrieve the actual role assingee, and let it's logic decide. RoleAssignee cra = provider.findRoleAssignee(craIdtf); if ( cra != null ) { if ( cra instanceof Group ) { Group cgrp = (Group) cra; if ( cgrp.contains(req) ) { return true; } } // if cra is a user, we would have returned after the .contains() test. } } // If we get here, the request is not in this group. return false; } /** * @param req * @return {@code true} iff the request if contained in an explicit group that's a member of this group. */ private boolean containsIndirectly(DataverseRequest req) { for ( ExplicitGroup ceg : containedExplicitGroups ) { if ( ceg.contains(req) ) { return true; } } return false; } /** * Updates the alias of the group. Call this after setting the owner or the * groupAliasInOwner fields. JPA-related activities call this automatically. */ public void updateAlias() { groupAlias = ((getOwner()!=null) ? Long.toString(getOwner().getId()) + "-" : "") + getGroupAliasInOwner(); } @PrePersist void prepersist() { updateAlias(); } @PostLoad void postload() { updateAlias(); } @Override public boolean isEditable() { return true; } @Override public ExplicitGroupProvider getGroupProvider() { return provider; } void setProvider( ExplicitGroupProvider c ) { provider = c; } @Override public String getIdentifier() { return Group.IDENTIFIER_PREFIX + provider.getGroupProviderAlias() + Group.PATH_SEPARATOR + getAlias(); } @Override public RoleAssigneeDisplayInfo getDisplayInfo() { return new RoleAssigneeDisplayInfo(getDisplayName(), null); } public String getGroupAliasInOwner() { return groupAliasInOwner; } public void setGroupAliasInOwner(String groupAliasInOwner) { this.groupAliasInOwner = groupAliasInOwner; } @Override public String getAlias() { return groupAlias; } @Override public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public DvObject getOwner() { return owner; } public void setOwner(DvObject owner) { this.owner = owner; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public int hashCode() { int hash = 7; hash = 53 * hash + Objects.hashCode(this.id); hash = 53 * hash + Objects.hashCode(this.groupAliasInOwner); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if ( ! (obj instanceof ExplicitGroup)) { return false; } final ExplicitGroup other = (ExplicitGroup) obj; if ( id!=null && other.getId()!=null) { return Objects.equals(id, other.getId()); } else { return Objects.equals(this.groupAliasInOwner, other.groupAliasInOwner) && Objects.equals(this.owner, other.owner); } } /** * Low-level call to return the role assignee identifier strings. Note that * the role assignees themselves might be stale, which is why this call is here - * to allow the {@link ExplicitGroupServiceBean} to clean up this collection. * @return the strings of the role assignees in this group. */ Set<String> getContainedRoleAssignees() { return containedRoleAssignees; } @Override public String toString() { return "[ExplicitGroup " + groupAlias + "]"; } }