Skip to content

Commit 106fb6f

Browse files
authored
Merge pull request #10906 from IQSS/6467-optimize-permission-lookups-for-a-user
Optimize permission lookups for a user
2 parents 3aea148 + c290e40 commit 106fb6f

File tree

10 files changed

+341
-38
lines changed

10 files changed

+341
-38
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The following API have been added:
2+
3+
/api/users/{identifier}/allowedCollections/{permission}
4+
5+
This API lists the dataverses/collections that the user has access to via the permission passed.
6+
By passing "any" as the permission the list will return all dataverse/collections that the user can access regardless of which permission is used.
7+
This API can be executed only by the User requesting their own list of accessible collections or by an Administrator.
8+
Valid Permissions are: AddDataverse, AddDataset, ViewUnpublishedDataverse, ViewUnpublishedDataset, DownloadFile, EditDataverse, EditDataset, ManageDataversePermissions,
9+
ManageDatasetPermissions, ManageFilePermissions, PublishDataverse, PublishDataset, DeleteDataverse, DeleteDatasetDraft, and "any" as a wildcard option.

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6501,6 +6501,27 @@ Example: List permissions a user (based on API Token used) has on a dataset whos
65016501
65026502
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/admin/permissions/:persistentId?persistentId=$PERSISTENT_IDENTIFIER"
65036503
6504+
List Dataverse collections a user can act on based on their permissions
6505+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6506+
6507+
List Dataverse collections a user can act on based on a particular permission ::
6508+
6509+
GET http://$SERVER/api/users/$identifier/allowedCollections/$permission
6510+
6511+
.. note:: This API can only be called by an Administrator or by a User requesting their own list of accessible collections.
6512+
6513+
The ``$identifier`` is the username of the requested user.
6514+
The ``$permission`` is the permission (tied to the roles) that gives the user access to the collection.
6515+
Passing ``$permission`` as 'any' will return the collection as long as the user has any access/permission on the collection
6516+
6517+
.. code-block:: bash
6518+
6519+
export SERVER_URL=https://demo.dataverse.org
6520+
export $USERNAME=jsmith
6521+
export PERMISSION=PublishDataverse
6522+
6523+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION"
6524+
65046525
Show Role Assignee
65056526
~~~~~~~~~~~~~~~~~~
65066527

src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
44
import edu.harvard.iq.dataverse.authorization.DataverseRole;
5+
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address;
6+
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Address;
7+
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
58
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
69
import edu.harvard.iq.dataverse.authorization.users.GuestUser;
710
import edu.harvard.iq.dataverse.authorization.Permission;
811
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
9-
import edu.harvard.iq.dataverse.authorization.groups.Group;
1012
import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean;
11-
import edu.harvard.iq.dataverse.authorization.groups.GroupUtil;
1213
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
1314
import edu.harvard.iq.dataverse.authorization.users.User;
1415
import edu.harvard.iq.dataverse.engine.command.Command;
@@ -37,7 +38,6 @@
3738
import java.util.Collections;
3839
import java.util.HashMap;
3940
import java.util.LinkedList;
40-
import java.util.logging.Level;
4141
import java.util.stream.Collectors;
4242
import static java.util.stream.Collectors.toList;
4343
import jakarta.persistence.Query;
@@ -100,6 +100,70 @@ public class PermissionServiceBean {
100100
@Inject
101101
DatasetVersionFilesServiceBean datasetVersionFilesServiceBean;
102102

103+
private static final String LIST_ALL_DATAVERSES_USER_HAS_PERMISSION = """
104+
WITH grouplist AS (
105+
SELECT explicitgroup_authenticateduser.explicitgroup_id as id FROM explicitgroup_authenticateduser
106+
WHERE explicitgroup_authenticateduser.containedauthenticatedusers_id = @USERID
107+
)
108+
109+
SELECT * FROM DATAVERSE WHERE id IN (
110+
SELECT definitionpoint_id
111+
FROM roleassignment
112+
WHERE roleassignment.assigneeidentifier IN (
113+
SELECT CONCAT('&explicit/', explicitgroup.groupalias) as assignee
114+
FROM explicitgroup
115+
WHERE explicitgroup.id IN (
116+
(
117+
SELECT explicitgroup.id id
118+
FROM explicitgroup
119+
WHERE EXISTS (SELECT id FROM grouplist WHERE id = explicitgroup.id)
120+
) UNION (
121+
SELECT explicitgroup_explicitgroup.containedexplicitgroups_id id
122+
FROM explicitgroup_explicitgroup
123+
WHERE EXISTS (SELECT id FROM grouplist WHERE id = explicitgroup_explicitgroup.explicitgroup_id)
124+
AND EXISTS (SELECT id FROM dataverserole
125+
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
126+
)
127+
)
128+
) UNION (
129+
SELECT definitionpoint_id
130+
FROM roleassignment
131+
WHERE roleassignment.assigneeidentifier = (
132+
SELECT CONCAT('@', authenticateduser.useridentifier)
133+
FROM authenticateduser
134+
WHERE authenticateduser.id = @USERID)
135+
AND EXISTS (SELECT id FROM dataverserole
136+
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
137+
) UNION (
138+
SELECT definitionpoint_id
139+
FROM roleassignment
140+
WHERE roleassignment.assigneeidentifier = ':authenticated-users'
141+
AND EXISTS (SELECT id FROM dataverserole
142+
WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
143+
) UNION (
144+
SELECT definitionpoint_id
145+
FROM roleassignment
146+
WHERE roleassignment.assigneeidentifier IN (
147+
SELECT CONCAT('&shib/', persistedglobalgroup.persistedgroupalias) as assignee
148+
FROM persistedglobalgroup
149+
WHERE dtype = 'ShibGroup'
150+
AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
151+
)
152+
) UNION (
153+
SELECT definitionpoint_id
154+
FROM roleassignment
155+
WHERE roleassignment.assigneeidentifier IN (
156+
SELECT CONCAT('&ip/', persistedglobalgroup.persistedgroupalias) as assignee
157+
FROM persistedglobalgroup
158+
LEFT OUTER JOIN ipv4range ON persistedglobalgroup.id = ipv4range.owner_id
159+
LEFT OUTER JOIN ipv6range ON persistedglobalgroup.id = ipv6range.owner_id
160+
WHERE dtype = 'IpGroup'
161+
AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment.role_id AND (dataverserole.permissionbits & @PERMISSIONBIT !=0))
162+
AND @IPRANGESQL
163+
)
164+
)
165+
)
166+
""";
103167
/**
104168
* A request-level permission query (e.g includes IP ras).
105169
*/
@@ -553,36 +617,6 @@ public RequestPermissionQuery request(DataverseRequest req) {
553617
return new RequestPermissionQuery(null, req);
554618
}
555619

556-
/**
557-
* Go from (User, Permission) to a list of Dataverse objects that the user
558-
has the permission on.
559-
*
560-
* @param user
561-
* @param permission
562-
* @return The list of dataverses {@code user} has permission
563-
{@code permission} on.
564-
*/
565-
public List<Dataverse> getDataversesUserHasPermissionOn(AuthenticatedUser user, Permission permission) {
566-
Set<Group> groups = groupService.groupsFor(user);
567-
String identifiers = GroupUtil.getAllIdentifiersForUser(user, groups);
568-
/**
569-
* @todo Are there any strings in identifiers that would break this SQL
570-
* query?
571-
*/
572-
String query = "SELECT id FROM dvobject WHERE dtype = 'Dataverse' and id in (select definitionpoint_id from roleassignment where assigneeidentifier in (" + identifiers + "));";
573-
logger.log(Level.FINE, "query: {0}", query);
574-
Query nativeQuery = em.createNativeQuery(query);
575-
List<Integer> dataverseIdsToCheck = nativeQuery.getResultList();
576-
List<Dataverse> dataversesUserHasPermissionOn = new LinkedList<>();
577-
for (int dvIdAsInt : dataverseIdsToCheck) {
578-
Dataverse dataverse = dataverseService.find(Long.valueOf(dvIdAsInt));
579-
if (userOn(user, dataverse).has(permission)) {
580-
dataversesUserHasPermissionOn.add(dataverse);
581-
}
582-
}
583-
return dataversesUserHasPermissionOn;
584-
}
585-
586620
public List<AuthenticatedUser> getUsersWithPermissionOn(Permission permission, DvObject dvo) {
587621
List<AuthenticatedUser> usersHasPermissionOn = new LinkedList<>();
588622
Set<RoleAssignment> ras = roleService.rolesAssignments(dvo);
@@ -888,4 +922,46 @@ private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion
888922
Long result = em.createQuery(criteriaQuery).getSingleResult();
889923
return result > 0;
890924
}
925+
926+
public List<Dataverse> findPermittedCollections(DataverseRequest request, AuthenticatedUser user, Permission permission) {
927+
return findPermittedCollections(request, user, 1 << permission.ordinal());
928+
}
929+
public List<Dataverse> findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit) {
930+
if (user != null) {
931+
// IP Group - Only check IP if a User is calling for themself
932+
String ipRangeSQL = "FALSE";
933+
if (request != null
934+
&& request.getAuthenticatedUser() != null
935+
&& request.getSourceAddress() != null
936+
&& request.getAuthenticatedUser().getUserIdentifier().equalsIgnoreCase(user.getUserIdentifier())) {
937+
IpAddress ip = request.getSourceAddress();
938+
if (ip instanceof IPv4Address) {
939+
IPv4Address ipv4 = (IPv4Address) ip;
940+
ipRangeSQL = ipv4.toBigInteger() + " BETWEEN ipv4range.bottomaslong AND ipv4range.topaslong";
941+
} else if (ip instanceof IPv6Address) {
942+
IPv6Address ipv6 = (IPv6Address) ip;
943+
long[] vals = ipv6.toLongArray();
944+
if (vals.length == 4) {
945+
ipRangeSQL = """
946+
(@0 BETWEEN ipv6range.bottoma AND ipv6range.topa
947+
AND @1 BETWEEN ipv6range.bottomb AND ipv6range.topb
948+
AND @2 BETWEEN ipv6range.bottomc AND ipv6range.topc
949+
AND @3 BETWEEN ipv6range.bottomd AND ipv6range.topd)
950+
""";
951+
for (int i = 0; i < vals.length; i++) {
952+
ipRangeSQL = ipRangeSQL.replace("@" + i, String.valueOf(vals[i]));
953+
}
954+
}
955+
}
956+
}
957+
958+
String sqlCode = LIST_ALL_DATAVERSES_USER_HAS_PERMISSION
959+
.replace("@USERID", String.valueOf(user.getId()))
960+
.replace("@PERMISSIONBIT", String.valueOf(permissionBit))
961+
.replace("@IPRANGESQL", ipRangeSQL);
962+
return em.createNativeQuery(sqlCode, Dataverse.class).getResultList();
963+
}
964+
return null;
965+
}
891966
}
967+

src/main/java/edu/harvard/iq/dataverse/api/Users.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,29 @@ public Response getTracesElement(@Context ContainerRequestContext crc, @Context
270270
}
271271
}
272272

273+
@GET
274+
@AuthRequired
275+
@Path("{identifier}/allowedCollections/{permission}")
276+
@Produces("application/json")
277+
public Response getUserPermittedCollections(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("permission") String permission) {
278+
AuthenticatedUser authenticatedUser = null;
279+
try {
280+
authenticatedUser = getRequestAuthenticatedUserOrDie(crc);
281+
if (!authenticatedUser.getUserIdentifier().equalsIgnoreCase(identifier) && !authenticatedUser.isSuperuser()) {
282+
return error(Response.Status.FORBIDDEN, "This API call can be used by Users getting there own permitted collections or by superusers.");
283+
}
284+
} catch (WrappedResponse ex) {
285+
return error(Response.Status.UNAUTHORIZED, "Authentication is required.");
286+
}
287+
try {
288+
AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier);
289+
JsonObjectBuilder jsonObj = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission));
290+
return ok(jsonObj);
291+
} catch (WrappedResponse ex) {
292+
return ex.getResponse();
293+
}
294+
}
295+
273296
@POST
274297
@Path("register")
275298
public Response registerOIDCUser(String body) {

src/main/java/edu/harvard/iq/dataverse/api/datadeposit/SWORDv2ServiceDocumentServlet.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package edu.harvard.iq.dataverse.api.datadeposit;
22

33
import java.io.IOException;
4+
5+
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
46
import jakarta.inject.Inject;
57
import jakarta.servlet.ServletException;
68
import jakarta.servlet.http.HttpServletRequest;
@@ -29,6 +31,7 @@ public void init() throws ServletException {
2931

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

src/main/java/edu/harvard/iq/dataverse/api/datadeposit/ServiceDocumentManagerImpl.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import edu.harvard.iq.dataverse.DataverseServiceBean;
55
import edu.harvard.iq.dataverse.PermissionServiceBean;
66
import edu.harvard.iq.dataverse.authorization.Permission;
7+
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
78
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
9+
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
810
import edu.harvard.iq.dataverse.util.SystemConfig;
911
import java.util.List;
1012
import java.util.logging.Logger;
@@ -37,6 +39,8 @@ public class ServiceDocumentManagerImpl implements ServiceDocumentManager {
3739
@Inject
3840
UrlManager urlManager;
3941

42+
private IpAddress ipAddress = null;
43+
4044
@Override
4145
public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCredentials, SwordConfiguration config)
4246
throws SwordError, SwordServerException, SwordAuthException {
@@ -65,7 +69,7 @@ public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCred
6569
* shibIdentityProvider String on AuthenticatedUser is only set when a
6670
* SAML assertion is made at runtime via the browser.
6771
*/
68-
List<Dataverse> dataverses = permissionService.getDataversesUserHasPermissionOn(user, Permission.AddDataset);
72+
List<Dataverse> dataverses = permissionService.findPermittedCollections(new DataverseRequest(user, ipAddress), user, Permission.AddDataset);
6973
for (Dataverse dataverse : dataverses) {
7074
String dvAlias = dataverse.getAlias();
7175
if (dvAlias != null && !dvAlias.isEmpty()) {
@@ -82,4 +86,7 @@ public ServiceDocument getServiceDocument(String sdUri, AuthCredentials authCred
8286
return service;
8387
}
8488

89+
public void setIpAddress(IpAddress ipAddress) {
90+
this.ipAddress = ipAddress;
91+
}
8592
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package edu.harvard.iq.dataverse.engine.command.impl;
2+
3+
import edu.harvard.iq.dataverse.Dataverse;
4+
import edu.harvard.iq.dataverse.DvObject;
5+
import edu.harvard.iq.dataverse.authorization.Permission;
6+
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
7+
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
8+
import edu.harvard.iq.dataverse.engine.command.AbstractCommand;
9+
import edu.harvard.iq.dataverse.engine.command.CommandContext;
10+
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
11+
import edu.harvard.iq.dataverse.engine.command.RequiredPermissions;
12+
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
13+
import jakarta.json.Json;
14+
import jakarta.json.JsonArrayBuilder;
15+
import jakarta.json.JsonObjectBuilder;
16+
17+
import java.util.List;
18+
import java.util.logging.Logger;
19+
20+
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json;
21+
22+
@RequiredPermissions({})
23+
public class GetUserPermittedCollectionsCommand extends AbstractCommand<JsonObjectBuilder> {
24+
private static final Logger logger = Logger.getLogger(GetUserPermittedCollectionsCommand.class.getCanonicalName());
25+
26+
private DataverseRequest request;
27+
private AuthenticatedUser user;
28+
private String permission;
29+
public GetUserPermittedCollectionsCommand(DataverseRequest request, AuthenticatedUser user, String permission) {
30+
super(request, (DvObject) null);
31+
this.request = request;
32+
this.user = user;
33+
this.permission = permission;
34+
}
35+
36+
@Override
37+
public JsonObjectBuilder execute(CommandContext ctxt) throws CommandException {
38+
if (user == null) {
39+
throw new CommandException("User not found.", this);
40+
}
41+
int permissionBit;
42+
try {
43+
permissionBit = permission.equalsIgnoreCase("any") ?
44+
Integer.MAX_VALUE : (1 << Permission.valueOf(permission).ordinal());
45+
} catch (IllegalArgumentException e) {
46+
throw new CommandException("Permission not valid.", this);
47+
}
48+
List<Dataverse> collections = ctxt.permissions().findPermittedCollections(request, user, permissionBit);
49+
if (collections != null) {
50+
JsonObjectBuilder job = Json.createObjectBuilder();
51+
JsonArrayBuilder jab = Json.createArrayBuilder();
52+
for (Dataverse dv : collections) {
53+
jab.add(json(dv));
54+
}
55+
job.add("count", collections.size());
56+
job.add("items", jab);
57+
return job;
58+
}
59+
return null;
60+
}
61+
}

0 commit comments

Comments
 (0)