Skip to content

Commit

Permalink
Merge pull request #10906 from IQSS/6467-optimize-permission-lookups-…
Browse files Browse the repository at this point in the history
…for-a-user

Optimize permission lookups for a user
  • Loading branch information
ofahimIQSS authored Feb 10, 2025
2 parents 3aea148 + c290e40 commit 106fb6f
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The following API have been added:

/api/users/{identifier}/allowedCollections/{permission}

This API lists the dataverses/collections that the user has access to via the permission passed.
By passing "any" as the permission the list will return all dataverse/collections that the user can access regardless of which permission is used.
This API can be executed only by the User requesting their own list of accessible collections or by an Administrator.
Valid Permissions are: AddDataverse, AddDataset, ViewUnpublishedDataverse, ViewUnpublishedDataset, DownloadFile, EditDataverse, EditDataset, ManageDataversePermissions,
ManageDatasetPermissions, ManageFilePermissions, PublishDataverse, PublishDataset, DeleteDataverse, DeleteDatasetDraft, and "any" as a wildcard option.
21 changes: 21 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6501,6 +6501,27 @@ Example: List permissions a user (based on API Token used) has on a dataset whos
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/admin/permissions/:persistentId?persistentId=$PERSISTENT_IDENTIFIER"
List Dataverse collections a user can act on based on their permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
List Dataverse collections a user can act on based on a particular permission ::
GET http://$SERVER/api/users/$identifier/allowedCollections/$permission
.. note:: This API can only be called by an Administrator or by a User requesting their own list of accessible collections.
The ``$identifier`` is the username of the requested user.
The ``$permission`` is the permission (tied to the roles) that gives the user access to the collection.
Passing ``$permission`` as 'any' will return the collection as long as the user has any access/permission on the collection
.. code-block:: bash
export SERVER_URL=https://demo.dataverse.org
export $USERNAME=jsmith
export PERMISSION=PublishDataverse
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION"
Show Role Assignee
~~~~~~~~~~~~~~~~~~
Expand Down
142 changes: 109 additions & 33 deletions src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Address;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
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;
Expand Down Expand Up @@ -37,7 +38,6 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import jakarta.persistence.Query;
Expand Down Expand Up @@ -100,6 +100,70 @@ public class PermissionServiceBean {
@Inject
DatasetVersionFilesServiceBean datasetVersionFilesServiceBean;

private static final String LIST_ALL_DATAVERSES_USER_HAS_PERMISSION = """
WITH grouplist AS (
SELECT explicitgroup_authenticateduser.explicitgroup_id as id FROM explicitgroup_authenticateduser
WHERE explicitgroup_authenticateduser.containedauthenticatedusers_id = @USERID
)
SELECT * FROM DATAVERSE WHERE id IN (
SELECT definitionpoint_id
FROM roleassignment
WHERE roleassignment.assigneeidentifier IN (
SELECT CONCAT('&explicit/', explicitgroup.groupalias) as assignee
FROM explicitgroup
WHERE explicitgroup.id IN (
(
SELECT explicitgroup.id id
FROM explicitgroup
WHERE EXISTS (SELECT id FROM grouplist WHERE id = explicitgroup.id)
) UNION (
SELECT explicitgroup_explicitgroup.containedexplicitgroups_id id
FROM explicitgroup_explicitgroup
WHERE EXISTS (SELECT id FROM grouplist WHERE id = explicitgroup_explicitgroup.explicitgroup_id)
AND EXISTS (SELECT id FROM dataverserole
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
)
)
) UNION (
SELECT definitionpoint_id
FROM roleassignment
WHERE roleassignment.assigneeidentifier = (
SELECT CONCAT('@', authenticateduser.useridentifier)
FROM authenticateduser
WHERE authenticateduser.id = @USERID)
AND EXISTS (SELECT id FROM dataverserole
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
) UNION (
SELECT definitionpoint_id
FROM roleassignment
WHERE roleassignment.assigneeidentifier = ':authenticated-users'
AND EXISTS (SELECT id FROM dataverserole
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
) UNION (
SELECT definitionpoint_id
FROM roleassignment
WHERE roleassignment.assigneeidentifier IN (
SELECT CONCAT('&shib/', persistedglobalgroup.persistedgroupalias) as assignee
FROM persistedglobalgroup
WHERE dtype = 'ShibGroup'
AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
)
) UNION (
SELECT definitionpoint_id
FROM roleassignment
WHERE roleassignment.assigneeidentifier IN (
SELECT CONCAT('&ip/', persistedglobalgroup.persistedgroupalias) as assignee
FROM persistedglobalgroup
LEFT OUTER JOIN ipv4range ON persistedglobalgroup.id = ipv4range.owner_id
LEFT OUTER JOIN ipv6range ON persistedglobalgroup.id = ipv6range.owner_id
WHERE dtype = 'IpGroup'
AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
AND @IPRANGESQL
)
)
)
""";
/**
* A request-level permission query (e.g includes IP ras).
*/
Expand Down Expand Up @@ -553,36 +617,6 @@ 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);
Expand Down Expand Up @@ -888,4 +922,46 @@ private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion
Long result = em.createQuery(criteriaQuery).getSingleResult();
return result > 0;
}

public List<Dataverse> findPermittedCollections(DataverseRequest request, AuthenticatedUser user, Permission permission) {
return findPermittedCollections(request, user, 1 << permission.ordinal());
}
public List<Dataverse> findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit) {
if (user != null) {
// IP Group - Only check IP if a User is calling for themself
String ipRangeSQL = "FALSE";
if (request != null
&& request.getAuthenticatedUser() != null
&& request.getSourceAddress() != null
&& request.getAuthenticatedUser().getUserIdentifier().equalsIgnoreCase(user.getUserIdentifier())) {
IpAddress ip = request.getSourceAddress();
if (ip instanceof IPv4Address) {
IPv4Address ipv4 = (IPv4Address) ip;
ipRangeSQL = ipv4.toBigInteger() + " BETWEEN ipv4range.bottomaslong AND ipv4range.topaslong";
} else if (ip instanceof IPv6Address) {
IPv6Address ipv6 = (IPv6Address) ip;
long[] vals = ipv6.toLongArray();
if (vals.length == 4) {
ipRangeSQL = """
(@0 BETWEEN ipv6range.bottoma AND ipv6range.topa
AND @1 BETWEEN ipv6range.bottomb AND ipv6range.topb
AND @2 BETWEEN ipv6range.bottomc AND ipv6range.topc
AND @3 BETWEEN ipv6range.bottomd AND ipv6range.topd)
""";
for (int i = 0; i < vals.length; i++) {
ipRangeSQL = ipRangeSQL.replace("@" + i, String.valueOf(vals[i]));
}
}
}
}

String sqlCode = LIST_ALL_DATAVERSES_USER_HAS_PERMISSION
.replace("@USERID", String.valueOf(user.getId()))
.replace("@PERMISSIONBIT", String.valueOf(permissionBit))
.replace("@IPRANGESQL", ipRangeSQL);
return em.createNativeQuery(sqlCode, Dataverse.class).getResultList();
}
return null;
}
}

23 changes: 23 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,29 @@ public Response getTracesElement(@Context ContainerRequestContext crc, @Context
}
}

@GET
@AuthRequired
@Path("{identifier}/allowedCollections/{permission}")
@Produces("application/json")
public Response getUserPermittedCollections(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("permission") String permission) {
AuthenticatedUser authenticatedUser = null;
try {
authenticatedUser = getRequestAuthenticatedUserOrDie(crc);
if (!authenticatedUser.getUserIdentifier().equalsIgnoreCase(identifier) && !authenticatedUser.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "This API call can be used by Users getting there own permitted collections or by superusers.");
}
} catch (WrappedResponse ex) {
return error(Response.Status.UNAUTHORIZED, "Authentication is required.");
}
try {
AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier);
JsonObjectBuilder jsonObj = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission));
return ok(jsonObj);
} catch (WrappedResponse ex) {
return ex.getResponse();
}
}

@POST
@Path("register")
public Response registerOIDCUser(String body) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package edu.harvard.iq.dataverse.api.datadeposit;

import java.io.IOException;

import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -29,6 +31,7 @@ public void init() throws ServletException {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
serviceDocumentManagerImpl.setIpAddress((new DataverseRequest(null, req)).getSourceAddress());
this.api.get(req, resp);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import edu.harvard.iq.dataverse.DataverseServiceBean;
import edu.harvard.iq.dataverse.PermissionServiceBean;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.util.List;
import java.util.logging.Logger;
Expand Down Expand Up @@ -37,6 +39,8 @@ public class ServiceDocumentManagerImpl implements ServiceDocumentManager {
@Inject
UrlManager urlManager;

private IpAddress ipAddress = null;

@Override
public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCredentials, SwordConfiguration config)
throws SwordError, SwordServerException, SwordAuthException {
Expand Down Expand Up @@ -65,7 +69,7 @@ public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCred
* shibIdentityProvider String on AuthenticatedUser is only set when a
* SAML assertion is made at runtime via the browser.
*/
List<Dataverse> dataverses = permissionService.getDataversesUserHasPermissionOn(user, Permission.AddDataset);
List<Dataverse> dataverses = permissionService.findPermittedCollections(new DataverseRequest(user, ipAddress), user, Permission.AddDataset);
for (Dataverse dataverse : dataverses) {
String dvAlias = dataverse.getAlias();
if (dvAlias != null && !dvAlias.isEmpty()) {
Expand All @@ -82,4 +86,7 @@ public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCred
return service;
}

public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package edu.harvard.iq.dataverse.engine.command.impl;

import edu.harvard.iq.dataverse.Dataverse;
import edu.harvard.iq.dataverse.DvObject;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.engine.command.AbstractCommand;
import edu.harvard.iq.dataverse.engine.command.CommandContext;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.RequiredPermissions;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import jakarta.json.Json;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObjectBuilder;

import java.util.List;
import java.util.logging.Logger;

import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json;

@RequiredPermissions({})
public class GetUserPermittedCollectionsCommand extends AbstractCommand<JsonObjectBuilder> {
private static final Logger logger = Logger.getLogger(GetUserPermittedCollectionsCommand.class.getCanonicalName());

private DataverseRequest request;
private AuthenticatedUser user;
private String permission;
public GetUserPermittedCollectionsCommand(DataverseRequest request, AuthenticatedUser user, String permission) {
super(request, (DvObject) null);
this.request = request;
this.user = user;
this.permission = permission;
}

@Override
public JsonObjectBuilder execute(CommandContext ctxt) throws CommandException {
if (user == null) {
throw new CommandException("User not found.", this);
}
int permissionBit;
try {
permissionBit = permission.equalsIgnoreCase("any") ?
Integer.MAX_VALUE : (1 << Permission.valueOf(permission).ordinal());
} catch (IllegalArgumentException e) {
throw new CommandException("Permission not valid.", this);
}
List<Dataverse> collections = ctxt.permissions().findPermittedCollections(request, user, permissionBit);
if (collections != null) {
JsonObjectBuilder job = Json.createObjectBuilder();
JsonArrayBuilder jab = Json.createArrayBuilder();
for (Dataverse dv : collections) {
jab.add(json(dv));
}
job.add("count", collections.size());
job.add("items", jab);
return job;
}
return null;
}
}
Loading

0 comments on commit 106fb6f

Please sign in to comment.