package edu.harvard.iq.dataverse;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
import edu.harvard.iq.dataverse.authorization.users.GuestUser;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.groups.Group;
import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean;
import edu.harvard.iq.dataverse.authorization.groups.GroupUtil;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.engine.command.Command;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.HashSet;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import static edu.harvard.iq.dataverse.engine.command.CommandHelper.CH;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.persistence.Query;
/**
* Your one-stop-shop for deciding which user can do what action on which
* objects (TM). Note that this bean accesses the permissions/user assignment on
* a read-only basis. Changing the permissions a user has is done via roles and
* groups, over at {@link DataverseRoleServiceBean}.
*
* @author michael
*/
@Stateless
@Named
public class PermissionServiceBean {
private static final Logger logger = Logger.getLogger(PermissionServiceBean.class.getName());
private static final Set<Permission> PERMISSIONS_FOR_AUTHENTICATED_USERS_ONLY =
EnumSet.copyOf(Arrays.asList(Permission.values()).stream()
.filter( Permission::requiresAuthenticatedUser )
.collect( Collectors.toList() ));
@EJB
BuiltinUserServiceBean userService;
@EJB
AuthenticationServiceBean authenticationService;
@EJB
DataverseRoleServiceBean roleService;
@EJB
RoleAssigneeServiceBean roleAssigneeService;
@EJB
DataverseServiceBean dataverseService;
@PersistenceContext
EntityManager em;
@EJB
GroupServiceBean groupService;
@Inject
DataverseSession session;
@Inject
DataverseRequestServiceBean dvRequestService;
/**
* A request-level permission query (e.g includes IP groups).
*/
public class RequestPermissionQuery {
final DvObject subject;
final DataverseRequest request;
public RequestPermissionQuery(DvObject subject, DataverseRequest request) {
this.subject = subject;
this.request = request;
}
public Set<Permission> get() {
return PermissionServiceBean.this.permissionsFor(request, subject);
}
public boolean has(Permission p) {
return get().contains(p);
}
public RequestPermissionQuery on( DvObject dvo ) {
return new RequestPermissionQuery(dvo, request);
}
/**
* Tests whether a command of the passed class can be issued over the {@link DvObject}
* in the context of the current request. Note that since some commands have dynamic permissions,
* in some cases it's better to instantiate a command object and pass it to {@link #canIssue(edu.harvard.iq.dataverse.engine.command.Command)}.
* @param aCmdClass
* @return {@code true} iff instances of the command class can be issued in the context of the current request.
*/
public boolean canIssue( Class<? extends Command> aCmdClass ) {
Map<String, Set<Permission>> required = CH.permissionsRequired(aCmdClass);
if (required.isEmpty() || required.get("") == null) {
logger.fine("IsUserAllowedOn: empty-true");
return true;
} else {
Set<Permission> grantedUserPermissions = permissionsFor(request, subject);
Set<Permission> requiredPermissionSet = required.get("");
return grantedUserPermissions.containsAll(requiredPermissionSet);
}
}
/**
* Tests whether the command can be issued over the {@link DvObject}
* in the context of the current request.
* @param aCmd
* @return {@code true} iff the command can be issued in the context of the current request.
*/
public boolean canIssue( Command<?> aCmd ) {
Map<String, Set<Permission>> required = aCmd.getRequiredPermissions();
if (required.isEmpty() || required.get("") == null) {
logger.fine("IsUserAllowedOn: empty-true");
return true;
} else {
Set<Permission> grantedUserPermissions = permissionsFor(request, subject);
Set<Permission> requiredPermissionSet = required.get("");
return grantedUserPermissions.containsAll(requiredPermissionSet);
}
}
}
/**
* A permission query for a given role assignee. Does not cover request-level permissions.
*/
public class StaticPermissionQuery {
final DvObject subject;
final RoleAssignee user;
private StaticPermissionQuery(RoleAssignee user, DvObject subject) {
this.subject = subject;
this.user = user;
}
public StaticPermissionQuery user(RoleAssignee anotherUser) {
return new StaticPermissionQuery(anotherUser, subject);
}
/**
* "Fast and loose" query mechanism, allowing to pass the command class
* name, does not take request-level permissions into account. Command is assumed to live in
* {@code edu.harvard.iq.dataverse.engine.command.impl.}
*
* @deprecated Use DynamicPermissionQuery instead
* @param commandName
* @return {@code true} iff the user has the permissions required by the
* command on the object.
* @throws ClassNotFoundException
*/
@Deprecated
public boolean canIssueCommand(String commandName) throws ClassNotFoundException {
return isUserAllowedOn(user,
(Class<? extends Command>) Class.forName("edu.harvard.iq.dataverse.engine.command.impl." + commandName), subject);
}
public Set<Permission> get() {
return permissionsFor(user, subject);
}
public boolean has(Permission p) {
return get().contains(p);
}
public boolean has(String pName) {
return get().contains(Permission.valueOf(pName));
}
}
public List<RoleAssignment> assignmentsOn(DvObject d) {
return em.createNamedQuery("RoleAssignment.listByDefinitionPointId", RoleAssignment.class)
.setParameter("definitionPointId", d.getId()).getResultList();
}
/**
* Finds all the permissions the {@link User} in {@code req} has over
* {@code dvo}, in the context of {@code req}.
* @param req
* @param dvo
* @return Permissions of {@code req.getUser()} over {@code dvo}.
*/
public Set<Permission> permissionsFor( DataverseRequest req, DvObject dvo ) {
Set<Permission> permissions = EnumSet.noneOf(Permission.class);
// Add permissions specifically given to the user
permissions.addAll( permissionsForSingleRoleAssignee(req.getUser(),dvo) );
Set<Group> groups = groupService.groupsFor(req,dvo);
// Add permissions gained from groups
for ( Group g : groups ) {
final Set<Permission> groupPremissions = permissionsForSingleRoleAssignee(g,dvo);
permissions.addAll(groupPremissions);
}
if ( ! req.getUser().isAuthenticated() ) {
permissions.removeAll( PERMISSIONS_FOR_AUTHENTICATED_USERS_ONLY );
}
return permissions;
}
/**
* Returns the set of permission a user/group has over a dataverse object.
* This method takes into consideration group memberships as well, but does
* not look into request-level groups.
* @param ra The role assignee.
* @param dvo The {@link DvObject} on which the user wants to operate
* @return the set of permissions {@code ra} has over {@code dvo}.
*/
public Set<Permission> permissionsFor(RoleAssignee ra, DvObject dvo) {
Set<Permission> permissions = EnumSet.noneOf(Permission.class);
// Add permissions specifically given to the user
permissions.addAll( permissionsForSingleRoleAssignee(ra,dvo) );
// Add permissions gained from groups
Set<Group> groupsRaBelongsTo = groupService.groupsFor(ra,dvo);
for ( Group g : groupsRaBelongsTo ) {
permissions.addAll( permissionsForSingleRoleAssignee(g,dvo) );
}
if ( (ra instanceof User) && (! ((User)ra).isAuthenticated()) ) {
permissions.removeAll( PERMISSIONS_FOR_AUTHENTICATED_USERS_ONLY );
}
return permissions;
}
private Set<Permission> permissionsForSingleRoleAssignee(RoleAssignee ra, DvObject d) {
// super user check
// for 4.0, we are allowing superusers all permissions
// for secure data, we may need to restrict some of the permissions
if (ra instanceof AuthenticatedUser && ((AuthenticatedUser) ra).isSuperuser()) {
return EnumSet.allOf(Permission.class);
}
// Start with no permissions, build from there.
Set<Permission> retVal = EnumSet.noneOf(Permission.class);
// File special case.
if (d instanceof DataFile) {
// unrestricted files that are part of a release dataset
// automatically get download permission for everybody:
// -- L.A. 4.0 beta12
DataFile df = (DataFile)d;
if (!df.isRestricted()) {
if (df.getOwner().getReleasedVersion() != null) {
if (df.getOwner().getReleasedVersion().getFileMetadatas() != null) {
for (FileMetadata fm : df.getOwner().getReleasedVersion().getFileMetadatas()) {
if (df.equals(fm.getDataFile())) {
retVal.add(Permission.DownloadFile);
break;
}
}
}
}
}
}
// Direct assignments to ra on d
assignmentsFor(ra, d).forEach(
asmnt -> retVal.addAll(asmnt.getRole().permissions())
);
// Recurse up the group containment hierarchy.
groupService.groupsFor(ra, d).forEach(
grp -> retVal.addAll(permissionsForSingleRoleAssignee(grp, d)));
return retVal;
}
/**
* Returns all the role assignments that are effective for {@code ra} over
* {@code d}. Traverses the containment hierarchy of the {@code d}.
* @param ra The role assignee whose role assignemnts we look for.
* @param d The dataverse object over which the roles are assigned
* @return A set of all the role assignments for {@code ra} over {@code d}.
*/
public Set<RoleAssignment> assignmentsFor(RoleAssignee ra, DvObject d) {
Set<RoleAssignment> assignments = new HashSet<>();
while (d != null) {
assignments.addAll(roleService.directRoleAssignments(ra, d));
if (d instanceof Dataverse && ((Dataverse) d).isEffectivelyPermissionRoot()) {
return assignments;
} else {
d = d.getOwner();
}
}
return assignments;
}
/**
* For commands with no named dvObjects, this allows a quick check whether
* a user can issue the command on the dataverse or not.
*
* @param u
* @param commandClass
* @param dvo
* @return
* @deprecated As commands have dynamic permissions now, it is not enough to look at the static permissions anymore.
* @see #isUserAllowedOn(edu.harvard.iq.dataverse.authorization.RoleAssignee, edu.harvard.iq.dataverse.engine.command.Command, edu.harvard.iq.dataverse.DvObject)
*/
public boolean isUserAllowedOn(RoleAssignee u, Class<? extends Command> commandClass, DvObject dvo) {
Map<String, Set<Permission>> required = CH.permissionsRequired(commandClass);
return isUserAllowedOn(u, required, dvo);
}
public boolean isUserAllowedOn(RoleAssignee u, Command<?> command, DvObject dvo) {
Map<String, Set<Permission>> required = command.getRequiredPermissions();
return isUserAllowedOn(u, required, dvo);
}
private boolean isUserAllowedOn(RoleAssignee u, Map<String, Set<Permission>> required, DvObject dvo) {
if (required.isEmpty() || required.get("") == null) {
logger.fine("IsUserAllowedOn: empty-true");
return true;
} else {
Set<Permission> grantedUserPermissions = permissionsFor(u, dvo);
Set<Permission> requiredPermissionSet = required.get("");
return grantedUserPermissions.containsAll(requiredPermissionSet);
}
}
public StaticPermissionQuery userOn(RoleAssignee u, DvObject d) {
if (u == null) {
// get guest user for dataverse d
u = GuestUser.get();
}
return new StaticPermissionQuery(u, d);
}
public RequestPermissionQuery on(DvObject d) {
if (d == null) {
throw new IllegalArgumentException("Cannot query permissions on a null DvObject");
}
if (d.getId() == null) {
throw new IllegalArgumentException("Cannot query permissions on a DvObject with a null id.");
}
return requestOn(dvRequestService.getDataverseRequest(), d);
}
public RequestPermissionQuery requestOn( DataverseRequest req, DvObject dvo ) {
if (dvo.getId() == null) {
throw new IllegalArgumentException("Cannot query permissions on a DvObject with a null id.");
}
return new RequestPermissionQuery(dvo, req);
}
public RequestPermissionQuery request( DataverseRequest req ) {
return new RequestPermissionQuery(null, req);
}
/**
* Go from (User, Permission) to a list of Dataverse objects that the user
* has the permission on.
*
* @param user
* @param permission
* @return The list of dataverses {@code user} has permission {@code permission} on.
*/
public List<Dataverse> getDataversesUserHasPermissionOn(AuthenticatedUser user, Permission permission) {
Set<Group> groups = groupService.groupsFor(user);
String identifiers = GroupUtil.getAllIdentifiersForUser(user, groups);
/**
* @todo Are there any strings in identifiers that would break this SQL
* query?
*/
String query = "SELECT id FROM dvobject WHERE dtype = 'Dataverse' and id in (select definitionpoint_id from roleassignment where assigneeidentifier in (" + identifiers + "));";
logger.log(Level.FINE, "query: {0}", query);
Query nativeQuery = em.createNativeQuery(query);
List<Integer> dataverseIdsToCheck = nativeQuery.getResultList();
List<Dataverse> dataversesUserHasPermissionOn = new LinkedList<>();
for (int dvIdAsInt : dataverseIdsToCheck) {
Dataverse dataverse = dataverseService.find(Long.valueOf(dvIdAsInt));
if (userOn(user, dataverse).has(permission)) {
dataversesUserHasPermissionOn.add(dataverse);
}
}
return dataversesUserHasPermissionOn;
}
public List<AuthenticatedUser> getUsersWithPermissionOn(Permission permission, DvObject dvo) {
List<AuthenticatedUser> usersHasPermissionOn = new LinkedList<>();
Set<RoleAssignment> ras = roleService.rolesAssignments(dvo);
for (RoleAssignment ra : ras) {
if (ra.getRole().permissions().contains(permission)) {
RoleAssignee raee = roleAssigneeService.getRoleAssignee(ra.getAssigneeIdentifier());
usersHasPermissionOn.addAll(roleAssigneeService.getExplicitUsers(raee));
}
}
return usersHasPermissionOn;
}
public List<Long> getDvObjectsUserHasRoleOn(User user) {
return getDvObjectIdsUserHasRoleOn(user, null, null, false);
}
public List<Long> getDvObjectIdsUserHasRoleOn(User user, List<DataverseRole> roles) {
return getDvObjectIdsUserHasRoleOn(user, roles, null, false);
}
/*
Method takes in a user and optional list of roles and dvobject type
queries the role assigment table filtering by optional roles and dv
returns dvobject ids
*/
private String getRolesClause(List<DataverseRole> roles) {
StringBuilder roleStringBld = new StringBuilder();
if (roles != null && !roles.isEmpty()) {
roleStringBld.append(" and role_id in (");
boolean first = true;
for (DataverseRole role : roles) {
if (!first) {
roleStringBld.append(",");
}
roleStringBld.append(role.getId());
first = false;
}
roleStringBld.append(")");
}
return roleStringBld.toString();
}
private String getTypesClause(List<String> types) {
boolean firstType = true;
StringBuilder typeStringBld = new StringBuilder();
if (types != null && !types.isEmpty()) {
typeStringBld.append(" dtype in (");
for (String type : types) {
if (!firstType) {
typeStringBld.append(",");
}
typeStringBld.append("'").append(type).append("'");
}
typeStringBld.append(") and ");
}
return typeStringBld.toString();
}
public List<Long> getDvObjectIdsUserHasRoleOn(User user, List<DataverseRole> roles, List<String> types, boolean indirect) {
String roleString = getRolesClause (roles);
String typeString = getTypesClause(types);
Query nativeQuery = em.createNativeQuery("SELECT id FROM dvobject WHERE "
+ typeString + " id in (select definitionpoint_id from roleassignment where assigneeidentifier in ('" + user.getIdentifier() + "') "
+ roleString + ");");
List<Integer> dataverseIdsToCheck = nativeQuery.getResultList();
List<Long> dataversesUserHasPermissionOn = new LinkedList<>();
String indirectParentIds = "";
Boolean indirectFirst = true;
for (int dvIdAsInt : dataverseIdsToCheck) {
dataversesUserHasPermissionOn.add(Long.valueOf(dvIdAsInt));
if (indirect) {
if (indirectFirst) {
indirectParentIds = "(" + Integer.toString(dvIdAsInt);
indirectFirst = false;
} else {
indirectParentIds += ", " + Integer.toString(dvIdAsInt);
}
}
}
// Get child datasets and files
if (indirect) {
indirectParentIds += ") ";
Query nativeQueryIndirect = em.createNativeQuery("SELECT id FROM dvobject WHERE "
+ " owner_id in " + indirectParentIds + " and dType = 'Dataset'; ");
List<Integer> childDatasetIds = nativeQueryIndirect.getResultList();
String indirectDatasetParentIds = "";
Boolean indirectFileFirst = true;
for (int dvIdAsInt : childDatasetIds) {
dataversesUserHasPermissionOn.add(Long.valueOf(dvIdAsInt));
if (indirect) {
if (indirectFileFirst) {
indirectDatasetParentIds = "(" + Integer.toString(dvIdAsInt);
indirectFileFirst = false;
} else {
indirectDatasetParentIds += ", " + Integer.toString(dvIdAsInt);
}
}
}
Query nativeQueryFileIndirect = em.createNativeQuery("SELECT id FROM dvobject WHERE "
+ " owner_id in " + indirectDatasetParentIds + " and dType = 'DataFile'; ");
List<Integer> childFileIds = nativeQueryFileIndirect.getResultList();
for (int dvIdAsInt : childFileIds) {
dataversesUserHasPermissionOn.add(Long.valueOf(dvIdAsInt));
}
}
return dataversesUserHasPermissionOn;
}
}