From 1832f927eed0311dce31fc3eae5ffd587388131c Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 12:20:21 +0200 Subject: [PATCH 01/10] feat: add APIs for listing user-assignable roles for datasets and dataverses --- .../iq/dataverse/RoleAssigneeServiceBean.java | 22 ++++++++++++++++++- .../harvard/iq/dataverse/api/Datasets.java | 7 ++++++ .../harvard/iq/dataverse/api/Dataverses.java | 7 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index c0b3a48d82a..11f9981b266 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -25,6 +25,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import edu.harvard.iq.dataverse.util.BitSet; import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.annotation.PostConstruct; import jakarta.ejb.EJB; @@ -59,6 +60,9 @@ public class RoleAssigneeServiceBean { @EJB DataverseRoleServiceBean dataverseRoleService; + @EJB + PermissionServiceBean permissionService; + protected Map predefinedRoleAssignees = new TreeMap<>(); @PostConstruct @@ -187,6 +191,23 @@ public List getSelectableDataverseRolesFor(DataverseRequest reque return assignedRoles.isEmpty() ? dataverseRoleService.findAll() : assignedRoles; } + public List getAssignableDataverseRolesFor(DataverseRequest request, DvObject dvo) { + // Get permissions the requesting user has for the dataset + Set granted = permissionService.permissionsFor(request, dvo); + BitSet grantedPermissionBits = new BitSet(); + for (Permission p : granted) { + grantedPermissionBits.set(p.ordinal()); + } + + // Get assignable roles (all roles that have AT MOST the granted permission bits set) + List retList = new ArrayList<>(); + String qstr = "select r.id from dataverserole r where (r.permissionbits & ?) = r.permissionbits"; + for (Object o : em.createNativeQuery(qstr).setParameter(1, grantedPermissionBits.getBits()).getResultList()) { + retList.add(dataverseRoleService.find(Long.valueOf((Integer) o))); + } + return retList; + } + public List getAssigneeDataverseRoleFor(DataverseRequest dataverseRequest) { if (dataverseRequest == null){ @@ -449,5 +470,4 @@ private void msgt(String s) { msg(s); msg("-------------------------------"); } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 1ada5f78b01..c94371906bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2193,6 +2193,13 @@ public Response getAssignments(@Context ContainerRequestContext crc, @PathParam( .stream().map(ra -> json(ra)).collect(toJsonArray())), getRequestUser(crc)); } + @GET + @AuthRequired + @Path("{identifier}/assignments/userAssignableRoles") + public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String id) { + return response(req -> ok(jsonDataverseRoles(roleAssigneeSvc.getAssignableDataverseRolesFor(req, findDatasetOrDie(id)))), getRequestUser(crc)); + } + @GET @AuthRequired @Deprecated(forRemoval = true, since = "2024-10-17") diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 7322a8c9341..8560661a587 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1318,6 +1318,13 @@ public Response listAssignments(@Context ContainerRequestContext crc, @PathParam ), getRequestUser(crc)); } + @GET + @AuthRequired + @Path("{identifier}/assignments/userAssignableRoles") + public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + return response(req -> ok(jsonDataverseRoles(roleAssigneeSvc.getAssignableDataverseRolesFor(req, findDataverseOrDie(dvIdtf)))), getRequestUser(crc)); + } + /** * This code for setting a dataverse logo via API was started when initially * investigating https://github.com/IQSS/dataverse/issues/3559 but it isn't From 64af01ce30353ec7dfe10d33aade1eeacb38d29b Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 16:46:03 +0200 Subject: [PATCH 02/10] feat: improve/refactor APIs for listing user-assignable roles for datasets and dataverses --- .../dataverse/DataverseRoleServiceBean.java | 71 ++++++++++++++++--- .../iq/dataverse/ManagePermissionsPage.java | 41 +++-------- .../iq/dataverse/RoleAssigneeServiceBean.java | 17 ----- .../harvard/iq/dataverse/api/Datasets.java | 2 +- .../harvard/iq/dataverse/api/Dataverses.java | 5 +- 5 files changed, 74 insertions(+), 62 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index c6be74af678..8d94c57f6a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -9,12 +9,8 @@ import edu.harvard.iq.dataverse.search.IndexResponse; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; + +import java.util.*; import java.util.logging.Logger; import java.util.stream.Collectors; import jakarta.ejb.EJB; @@ -47,6 +43,8 @@ public class DataverseRoleServiceBean implements java.io.Serializable { @EJB SolrIndexServiceBean solrIndexService; @EJB + PermissionServiceBean permissionService; + @EJB IndexAsync indexAsync; public DataverseRole save(DataverseRole aRole) { @@ -294,12 +292,10 @@ public List directRoleAssignments(DvObject dvo) { } /** - * Get all the available roles in a given dataverse, mapped by the dataverse - * that defines them. Map entries are ordered by reversed hierarchy (root is - * always last). + * Get all the available roles in a given dataverse. * * @param dvId The id of dataverse whose available roles we query - * @return map of available roles. + * @return Set of available roles */ public Set availableRoles(Long dvId) { Dataverse dv = em.find(Dataverse.class, dvId); @@ -314,6 +310,61 @@ public Set availableRoles(Long dvId) { return roles; } + /** + * Get all the available roles for a given Dataset, DataFile or Dataverse. + * This excludes roles that are not relevant to the given DvObject type (e.g. for Datasets, this excludes roles that + * only have Dataverse-level permissions). + * Currently, the available roles for Datasets and DataFiles are gotten from the collection they are in. + * + * @param dvo The Dataset, DataFile or Dataverse whose available roles we query + * @return Set of available roles + */ + public Set availableRoles(DvObject dvo) { + Set roles = new HashSet<>(); + + // Get roles available for given DvObject + if (dvo instanceof Dataverse) { + roles = availableRoles(dvo.getId()); + + } else if (dvo instanceof Dataset) { + roles = availableRoles(dvo.getOwner().getId()).stream() + .filter(role -> role.permissions().stream() + .anyMatch(p -> p.appliesTo(Dataset.class) + || p.appliesTo(DataFile.class))) + .collect(Collectors.toSet()); + + } else if (dvo instanceof DataFile) { + roles = availableRoles(dvo.getOwner().getOwner().getId()).stream() + .filter(role -> role.permissions().stream() + .anyMatch(p -> p.appliesTo(DataFile.class))) + .collect(Collectors.toSet()); + } + + return roles; + } + + /** + * Get all the available roles for a given Dataset, DataFile or Dataverse that can be assigned by a given User. + * This excludes roles that are not relevant to the given DvObject type (e.g. for Datasets, this excludes roles that + * only have Dataverse-level permissions). + * Currently, the available roles for Datasets and DataFiles are gotten from the collection they are in. + * + * @param dvo The Dataset, DataFile or Dataverse whose available roles we query + * @param user The user whose available roles we query + * @return Set of available roles + */ + public Set availableRoles(DvObject dvo, User user) { + Set roles = availableRoles(dvo); + + // Filter roles assignable by given user + Set granted = permissionService.permissionsFor(user, dvo); + roles = roles.stream() + .filter(role -> granted.containsAll(role.permissions())) + .collect(Collectors.toSet()); + + return roles; + } + public List getDataverseRolesByPermission(Permission permissionIn, Long ownerId) { /* For a given permission and dataverse Id get all of the roles (built-in or owned by the dataverse) diff --git a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java index 0e277c5aa32..231b345fa1b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java @@ -32,6 +32,8 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; + import jakarta.ejb.EJB; import jakarta.faces.application.FacesMessage; import jakarta.faces.event.ActionEvent; @@ -402,40 +404,13 @@ public List completeRoleAssignee( String query ) { } public List getAvailableRoles() { - List roles = new LinkedList<>(); - if (dvObject != null && dvObject.getId() != null) { - - if (dvObject instanceof Dataverse) { - roles.addAll(roleService.availableRoles(dvObject.getId())); - - } else if (dvObject instanceof Dataset) { - // don't show roles that only have Dataverse level permissions - // current the available roles for a dataset are gotten from its parent - for (DataverseRole role : roleService.availableRoles(dvObject.getOwner().getId())) { - for (Permission permission : role.permissions()) { - if (permission.appliesTo(Dataset.class) || permission.appliesTo(DataFile.class)) { - roles.add(role); - break; - } - } - } - - } else if (dvObject instanceof DataFile) { - // only show roles that have File level permissions - // current the available roles for a file are gotten from its parent's parent - for (DataverseRole role : roleService.availableRoles(dvObject.getOwner().getOwner().getId())) { - for (Permission permission : role.permissions()) { - if (permission.appliesTo(DataFile.class)) { - roles.add(role); - break; - } - } - } - } - - Collections.sort(roles, DataverseRole.CMP_BY_NAME); + if (dvObject == null || dvObject.getId() == null) { + return List.of(); } - return roles; + + return roleService.availableRoles(dvObject).stream() + .sorted(DataverseRole.CMP_BY_NAME) + .collect(Collectors.toList()); } public DataverseRole getAssignedRole() { diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index 11f9981b266..b17f6fa7688 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -191,23 +191,6 @@ public List getSelectableDataverseRolesFor(DataverseRequest reque return assignedRoles.isEmpty() ? dataverseRoleService.findAll() : assignedRoles; } - public List getAssignableDataverseRolesFor(DataverseRequest request, DvObject dvo) { - // Get permissions the requesting user has for the dataset - Set granted = permissionService.permissionsFor(request, dvo); - BitSet grantedPermissionBits = new BitSet(); - for (Permission p : granted) { - grantedPermissionBits.set(p.ordinal()); - } - - // Get assignable roles (all roles that have AT MOST the granted permission bits set) - List retList = new ArrayList<>(); - String qstr = "select r.id from dataverserole r where (r.permissionbits & ?) = r.permissionbits"; - for (Object o : em.createNativeQuery(qstr).setParameter(1, grantedPermissionBits.getBits()).getResultList()) { - retList.add(dataverseRoleService.find(Long.valueOf((Integer) o))); - } - return retList; - } - public List getAssigneeDataverseRoleFor(DataverseRequest dataverseRequest) { if (dataverseRequest == null){ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index c94371906bd..02099315036 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2197,7 +2197,7 @@ public Response getAssignments(@Context ContainerRequestContext crc, @PathParam( @AuthRequired @Path("{identifier}/assignments/userAssignableRoles") public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String id) { - return response(req -> ok(jsonDataverseRoles(roleAssigneeSvc.getAssignableDataverseRolesFor(req, findDatasetOrDie(id)))), getRequestUser(crc)); + return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDatasetOrDie(id), req.getUser())))), getRequestUser(crc)); } @GET diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 8560661a587..a4c512a8a7a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -120,6 +120,9 @@ public class Dataverses extends AbstractApiBean { @EJB DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; + + @EJB + DataverseRoleServiceBean dataverseRoleService; @POST @AuthRequired @@ -1322,7 +1325,7 @@ public Response listAssignments(@Context ContainerRequestContext crc, @PathParam @AuthRequired @Path("{identifier}/assignments/userAssignableRoles") public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { - return response(req -> ok(jsonDataverseRoles(roleAssigneeSvc.getAssignableDataverseRolesFor(req, findDataverseOrDie(dvIdtf)))), getRequestUser(crc)); + return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDataverseOrDie(dvIdtf), req.getUser())))), getRequestUser(crc)); } /** From db5ec514fe69e0199f15db5e23af1ef43f8b766d Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 16:49:08 +0200 Subject: [PATCH 03/10] test: add tests for APIs for listing user-assignable roles --- .../harvard/iq/dataverse/api/DatasetsIT.java | 107 ++++++++++++++++++ .../iq/dataverse/api/DataversesIT.java | 66 +++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 12 ++ 3 files changed, 185 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 16552869a80..e4874cdc162 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7019,6 +7019,113 @@ public void testUpdateMultipleFileMetadata() { .statusCode(OK.getStatusCode()); } + @Test + public void testListAssignableRoles() { + Response createUser = UtilIT.createRandomUser(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + // Create a collection (under root) + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + String dvAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + // Create dataset in that collection + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dvAlias, apiToken); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); + String datasetPersistentId = UtilIT.getDatasetPersistentIdFromResponse(createDatasetResponse); + + // List user-assignable roles for new dataset + Response listUserAssignableRolesResponse = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken); + listUserAssignableRolesResponse.prettyPrint(); + // All roles should be user-assignable, since the requesting user is admin of the collection + listUserAssignableRolesResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(5)) + .body("data[0].alias", equalTo(DataverseRole.ADMIN)) + .body("data[1].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[2].alias", equalTo(DataverseRole.EDITOR)) + .body("data[3].alias", equalTo(DataverseRole.CURATOR)) + .body("data[4].alias", equalTo(DataverseRole.MEMBER)); + + // Create second user + Response createUser2 = UtilIT.createRandomUser(); + String username2 = UtilIT.getUsernameFromResponse(createUser2); + String apiToken2 = UtilIT.getApiTokenFromResponse(createUser2); + + // List user-assignable roles for second user + Response listUser2AssignableRolesResponse = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken2); + listUser2AssignableRolesResponse.prettyPrint(); + // No roles should be user-assignable, since the requesting user has no permissions for the dataset + listUser2AssignableRolesResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(0)); + + // Assign role to second user for the dataset + Response grantRoleResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, "member", "@" + username2, apiToken); + grantRoleResponse.prettyPrint(); + assertEquals(200, grantRoleResponse.getStatusCode()); + + // List user-assignable roles for second user again + Response listUser2AssignableRolesResponse2 = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken2); + listUser2AssignableRolesResponse2.prettyPrint(); + // Now two roles should be user-assignable, since the requesting user has received some permissions for the dataset + listUser2AssignableRolesResponse2.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(2)) + .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + + // Create a second collection (under root) + Response createDataverse2Response = UtilIT.createRandomDataverse(apiToken); + String dvAlias2 = UtilIT.getAliasFromResponse(createDataverse2Response); + + // Create dataset in that collection + Response createDataset2Response = UtilIT.createRandomDatasetViaNativeApi(dvAlias2, apiToken); + Integer datasetId2 = UtilIT.getDatasetIdFromResponse(createDataset2Response); + + // List user-assignable roles for second user + Response listUser2AssignableRolesResponse3 = UtilIT.getUserAssignableRolesForDataset(datasetId2, apiToken2); + listUser2AssignableRolesResponse3.prettyPrint(); + // No roles should be user-assignable, since the requesting user has no permissions for the dataset + listUser2AssignableRolesResponse3.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(0)); + + // Assign role to second user for the new collection + Response grantRoleResponse2 = UtilIT.grantRoleOnDataverse(dvAlias2, "member", "@" + username2, apiToken); + grantRoleResponse2.prettyPrint(); + assertEquals(200, grantRoleResponse2.getStatusCode()); + + // List user-assignable roles for second user again + Response listUser2AssignableRolesResponse4 = UtilIT.getUserAssignableRolesForDataset(datasetId2, apiToken2); + listUser2AssignableRolesResponse4.prettyPrint(); + // Now two roles should be user-assignable, since the requesting user has received some permissions for the collection containing the dataset + listUser2AssignableRolesResponse4.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(2)) + .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + + // Clean up + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + assertEquals(200, destroyDatasetResponse.getStatusCode()); + + Response destroyDataset2Response = UtilIT.destroyDataset(datasetId2, apiToken); + assertEquals(200, destroyDataset2Response.getStatusCode()); + + Response deleteDataverseResponse = UtilIT.deleteDataverse(dvAlias, apiToken); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteDataverse2Response = UtilIT.deleteDataverse(dvAlias2, apiToken); + assertEquals(200, deleteDataverse2Response.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); + + Response deleteUser2Response = UtilIT.deleteUser(username2); + assertEquals(200, deleteUserResponse.getStatusCode()); + } + private String getSuperuserToken() { Response createResponse = UtilIT.createRandomUser(); String adminApiToken = UtilIT.getApiTokenFromResponse(createResponse); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index eb247ce6c7f..1814909a3bb 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.util.json.JsonParseException; import edu.harvard.iq.dataverse.util.json.JsonParser; @@ -2724,6 +2725,71 @@ public void testGetStorageDriver() { .statusCode(200); } + @Test + public void testListAssignableRoles() { + Response createUser = UtilIT.createRandomUser(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + // Create a collection (under root) + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + String dvAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + // List user-assignable roles for that collection + Response listUserAssignableRolesResponse = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken); + listUserAssignableRolesResponse.prettyPrint(); + // All roles should be user-assignable, since the requesting user is admin of the collection + listUserAssignableRolesResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(8)) + .body("data[0].alias", equalTo(DataverseRole.ADMIN)) + .body("data[1].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[2].alias", equalTo(DataverseRole.FULL_CONTRIBUTOR)) + .body("data[3].alias", equalTo(DataverseRole.DV_CONTRIBUTOR)) + .body("data[4].alias", equalTo(DataverseRole.DS_CONTRIBUTOR)) + .body("data[5].alias", equalTo(DataverseRole.EDITOR)) + .body("data[6].alias", equalTo(DataverseRole.CURATOR)) + .body("data[7].alias", equalTo(DataverseRole.MEMBER)); + + // Create second user + Response createUser2 = UtilIT.createRandomUser(); + String username2 = UtilIT.getUsernameFromResponse(createUser2); + String apiToken2 = UtilIT.getApiTokenFromResponse(createUser2); + + // List user-assignable roles for second user + Response listUser2AssignableRolesResponse = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); + listUser2AssignableRolesResponse.prettyPrint(); + // No roles should be user-assignable, since the requesting user has no permissions for the collection + listUser2AssignableRolesResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(0)); + + // Assign role to second user for the collection + Response grantRoleResponse = UtilIT.grantRoleOnDataverse(dvAlias, "member", "@" + username2, apiToken); + grantRoleResponse.prettyPrint(); + assertEquals(200, grantRoleResponse.getStatusCode()); + + // List user-assignable roles for second user again + Response listUser2AssignableRolesResponse2 = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); + listUser2AssignableRolesResponse2.prettyPrint(); + // Now two roles should be user-assignable, since the requesting user has received some permissions for the collection + listUser2AssignableRolesResponse2.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(2)) + .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + + // Clean up + Response deleteDataverseResponse = UtilIT.deleteDataverse(dvAlias, apiToken); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); + + Response deleteUser2Response = UtilIT.deleteUser(username2); + assertEquals(200, deleteUserResponse.getStatusCode()); + } + private String getSuperuserToken() { Response createResponse = UtilIT.createRandomUser(); String adminApiToken = UtilIT.getApiTokenFromResponse(createResponse); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index c11f66aa749..1bb124484ac 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2546,6 +2546,12 @@ static Response getRoleAssignmentsOnDataverse(String dataverseAliasOrId, String .header(API_TOKEN_HTTP_HEADER, apiToken) .get(url); } + + public static Response getUserAssignableRolesForDataverse(String dataverseAliasOrId, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/dataverses/" + dataverseAliasOrId + "/assignments/userAssignableRoles"); + } static Response updateDefaultContributorsRoleOnDataverse(String dataverseAliasOrId,String roleAlias, String apiToken) { String url = "/api/dataverses/" + dataverseAliasOrId + "/defaultContributorRole/" + roleAlias; @@ -2563,6 +2569,12 @@ static Response getRoleAssignmentsOnDataset(String datasetId, String persistentI .get(url); } + public static Response getUserAssignableRolesForDataset(Integer datasetId, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId + "/assignments/userAssignableRoles"); + } + static Response grantRoleOnDataset(String definitionPoint, String role, String roleAssignee, String apiToken) { JsonObjectBuilder roleBuilder = Json.createObjectBuilder(); From 5adf4623b0d699faa74d231eb88b9e9ce0289dfb Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 16:55:43 +0200 Subject: [PATCH 04/10] docs: add docs + release note for APIs for listing user-assignable roles --- .../11919-list-assignable-roles.md | 3 ++ doc/sphinx-guides/source/api/native-api.rst | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 doc/release-notes/11919-list-assignable-roles.md diff --git a/doc/release-notes/11919-list-assignable-roles.md b/doc/release-notes/11919-list-assignable-roles.md new file mode 100644 index 00000000000..48b0426186c --- /dev/null +++ b/doc/release-notes/11919-list-assignable-roles.md @@ -0,0 +1,3 @@ +There are two new API endpoints that list the roles a user can assign to others for a specific dataset or dataverse (`/api/datasets/$ID/assignments/userAssignableRoles` and `/api/dataverses/$ID/assignments/userAssignableRoles`). +They return only roles that the user is permitted to assign and that are relevant to the target dataset or dataverse. +Roles with higher permissions than the requesting user are excluded, and when querying for a dataset, roles that apply only at the dataverse level are filtered out. \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index a12dfa151c9..07a6741bb01 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -477,6 +477,27 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/dataverses/root/assignments" +.. _list-assignable-roles-on-a-dataverse-api: + +List Assignable Roles in a Dataverse Collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all the roles that are assignable by the current user at the given Dataverse collection: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/assignments/userAssignableRoles" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/dataverses/root/assignments/userAssignableRoles" + Assign Default Role to User Creating a Dataset in a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2432,6 +2453,27 @@ The fully expanded example above (without environment variables) looks like this .. code-block:: bash curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/datasets/2347/assignments" + +.. _list-assignable-roles-on-a-dataverse-api: + +List Assignable Roles in a Dataset +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all the roles that are assignable by the current user on the given dataset: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=2347 + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/datasets/$ID/assignments/userAssignableRoles" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/datasets/2347/assignments/userAssignableRoles" .. _assign-role-on-a-dataset-api: From 5d755423d97bf88b36705af04e52f945b177a126 Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 16:58:14 +0200 Subject: [PATCH 05/10] fix: revert unnecessary changes to RoleAssigneeServiceBean --- .../edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index b17f6fa7688..c0b3a48d82a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -25,7 +25,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import edu.harvard.iq.dataverse.util.BitSet; import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.annotation.PostConstruct; import jakarta.ejb.EJB; @@ -60,9 +59,6 @@ public class RoleAssigneeServiceBean { @EJB DataverseRoleServiceBean dataverseRoleService; - @EJB - PermissionServiceBean permissionService; - protected Map predefinedRoleAssignees = new TreeMap<>(); @PostConstruct @@ -453,4 +449,5 @@ private void msgt(String s) { msg(s); msg("-------------------------------"); } + } From 1d05e3e480f3b9805f931083d014b822f75a7504 Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 22 Oct 2025 17:00:29 +0200 Subject: [PATCH 06/10] fix: fix typo in Native API docs --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 07a6741bb01..f10e895d540 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2454,7 +2454,7 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/datasets/2347/assignments" -.. _list-assignable-roles-on-a-dataverse-api: +.. _list-assignable-roles-on-a-dataset-api: List Assignable Roles in a Dataset ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 8ff3b2482ac66bc5d305bc848af74192e84413ac Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Fri, 17 Jul 2026 11:20:10 +0200 Subject: [PATCH 07/10] docs: add API annotations for user-assignable role list APIs --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 7 ++++++- .../java/edu/harvard/iq/dataverse/api/Dataverses.java | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index d1953a8b5b2..c79787aefb2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2464,7 +2464,12 @@ public Response getAssignments(@Context ContainerRequestContext crc, @Parameter( @GET @AuthRequired @Path("{identifier}/assignments/userAssignableRoles") - public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String id) { + @Operation(summary = "Lists user-assignable roles", + description = "Lists the roles the current user is allowed to assign to others for a dataset.") + @APIResponse(responseCode = "200", + description = "List of user-assignable roles", + content = @Content(mediaType = "application/json")) + public Response getAssignableRoles(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String id) { return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDatasetOrDie(id), req.getUser())))), getRequestUser(crc)); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 791666c1c74..cab3528d2fb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -74,6 +74,8 @@ import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import jakarta.ws.rs.core.StreamingOutput; import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -1575,7 +1577,12 @@ public Response listAssignments(@Context ContainerRequestContext crc, @GET @AuthRequired @Path("{identifier}/assignments/userAssignableRoles") - public Response getAssignableRoles(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + @Operation(summary = "Lists user-assignable roles", + description = "Lists the roles the current user is allowed to assign to others for a dataverse.") + @APIResponse(responseCode = "200", + description = "List of user-assignable roles", + content = @Content(mediaType = "application/json")) + public Response getAssignableRoles(@Context ContainerRequestContext crc, @Parameter(description = "Dataverse alias, id, or persistent identifier.", required = true) @PathParam("identifier") String dvIdtf) { return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDataverseOrDie(dvIdtf), req.getUser())))), getRequestUser(crc)); } From 5c9c51bde13b6c6e875b70f0cd5e283ac104969c Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 9 Sep 2026 12:28:57 +0200 Subject: [PATCH 08/10] test: fix typos in testListAssignableRoles test cases --- src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java | 2 +- src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 71e372320f6..d7519d8f246 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7644,7 +7644,7 @@ public void testListAssignableRoles() { assertEquals(200, deleteUserResponse.getStatusCode()); Response deleteUser2Response = UtilIT.deleteUser(username2); - assertEquals(200, deleteUserResponse.getStatusCode()); + assertEquals(200, deleteUser2Response.getStatusCode()); } private String getSuperuserToken() { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index ff92ce5660d..2cddede825a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -3334,7 +3334,7 @@ public void testListAssignableRoles() { assertEquals(200, deleteUserResponse.getStatusCode()); Response deleteUser2Response = UtilIT.deleteUser(username2); - assertEquals(200, deleteUserResponse.getStatusCode()); + assertEquals(200, deleteUser2Response.getStatusCode()); } private String getSuperuserToken() { From ce281e54c357a23c2221db9660d06475796d2982 Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 9 Sep 2026 12:56:23 +0200 Subject: [PATCH 09/10] fix: update role filter logic and tests for user-assignable roles to check for manage permissions --- .../dataverse/DataverseRoleServiceBean.java | 20 ++++-- .../harvard/iq/dataverse/api/DatasetsIT.java | 69 ++++++++++++++++--- .../iq/dataverse/api/DataversesIT.java | 42 +++++++++-- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index 0bcff486426..a0d77523794 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -401,15 +401,21 @@ public Set availableRoles(DvObject dvo) { * @return Set of available roles */ public Set availableRoles(DvObject dvo, User user) { - Set roles = availableRoles(dvo); - - // Filter roles assignable by given user Set granted = permissionService.permissionsFor(user, dvo); - roles = roles.stream() - .filter(role -> granted.containsAll(role.permissions())) - .collect(Collectors.toSet()); - return roles; + Permission managePermission = dvo instanceof Dataverse + ? Permission.ManageDataversePermissions + : dvo instanceof Dataset + ? Permission.ManageDatasetPermissions + : Permission.ManageFilePermissions; + if (!granted.contains(managePermission)) { + return Set.of(); + } + + // Filter roles assignable by given user + return availableRoles(dvo).stream() + .filter(role -> granted.containsAll(role.permissions())) + .collect(Collectors.toSet()); } public List getDataverseRolesByPermission(Permission permissionIn, Long ownerId) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index d7519d8f246..35c210c71f4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7582,20 +7582,57 @@ public void testListAssignableRoles() { .body("data.size()", equalTo(0)); // Assign role to second user for the dataset - Response grantRoleResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, "member", "@" + username2, apiToken); + Response grantRoleResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, DataverseRole.MEMBER, "@" + username2, apiToken); grantRoleResponse.prettyPrint(); assertEquals(200, grantRoleResponse.getStatusCode()); // List user-assignable roles for second user again Response listUser2AssignableRolesResponse2 = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken2); listUser2AssignableRolesResponse2.prettyPrint(); - // Now two roles should be user-assignable, since the requesting user has received some permissions for the dataset + // No roles should be user-assignable, since the requesting user has no ManageDatasetPermissions permission listUser2AssignableRolesResponse2.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.size()", equalTo(2)) + .body("data.size()", equalTo(0)); + + // Grant a role which includes ManageDatasetPermissions + Response grantCuratorResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, DataverseRole.CURATOR, "@" + username2, apiToken); + grantCuratorResponse.prettyPrint(); + assertEquals(200, grantCuratorResponse.getStatusCode()); + + // A number of roles should now be assignable + Response listUser2AssignableRolesResponseWithManagePermission = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken2); + listUser2AssignableRolesResponseWithManagePermission.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(4)) .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) - .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + .body("data[1].alias", equalTo(DataverseRole.EDITOR)) + .body("data[2].alias", equalTo(DataverseRole.CURATOR)) + .body("data[3].alias", equalTo(DataverseRole.MEMBER)); + + // Grant an even more powerful role + Response grantAdminResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, DataverseRole.ADMIN, "@" + username2, apiToken); + grantAdminResponse.prettyPrint(); + assertEquals(200, grantAdminResponse.getStatusCode()); + + // All available roles should now be assignable + Response listUser2AssignableRolesResponseWithAdmin = UtilIT.getUserAssignableRolesForDataset(datasetId, apiToken2); + listUser2AssignableRolesResponseWithAdmin.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(5)) + .body("data[0].alias", equalTo(DataverseRole.ADMIN)) + .body("data[1].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[2].alias", equalTo(DataverseRole.EDITOR)) + .body("data[3].alias", equalTo(DataverseRole.CURATOR)) + .body("data[4].alias", equalTo(DataverseRole.MEMBER)); + + // Test that the requesting user can actually assign one of the returned roles to another user + Response createUser3 = UtilIT.createRandomUser(); + String username3 = UtilIT.getUsernameFromResponse(createUser3); + String returnedRole = listUser2AssignableRolesResponseWithAdmin.path("data[0].alias"); + Response assignReturnedRoleResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, returnedRole, "@" + username3, apiToken2); + assertEquals(200, assignReturnedRoleResponse.getStatusCode()); + // Now, we'll test user-assignable roles coming from roles assigned at the collection level // Create a second collection (under root) Response createDataverse2Response = UtilIT.createRandomDataverse(apiToken); String dvAlias2 = UtilIT.getAliasFromResponse(createDataverse2Response); @@ -7613,19 +7650,32 @@ public void testListAssignableRoles() { .body("data.size()", equalTo(0)); // Assign role to second user for the new collection - Response grantRoleResponse2 = UtilIT.grantRoleOnDataverse(dvAlias2, "member", "@" + username2, apiToken); + Response grantRoleResponse2 = UtilIT.grantRoleOnDataverse(dvAlias2, DataverseRole.MEMBER, "@" + username2, apiToken); grantRoleResponse2.prettyPrint(); assertEquals(200, grantRoleResponse2.getStatusCode()); // List user-assignable roles for second user again Response listUser2AssignableRolesResponse4 = UtilIT.getUserAssignableRolesForDataset(datasetId2, apiToken2); listUser2AssignableRolesResponse4.prettyPrint(); - // Now two roles should be user-assignable, since the requesting user has received some permissions for the collection containing the dataset + // No roles should be user-assignable, since the requesting user has no ManageDatasetPermissions permission listUser2AssignableRolesResponse4.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.size()", equalTo(2)) + .body("data.size()", equalTo(0)); + + // Grant a role which includes ManageDatasetPermissions + Response grantCuratorOnCollectionResponse = UtilIT.grantRoleOnDataverse(dvAlias2, DataverseRole.CURATOR, "@" + username2, apiToken); + grantCuratorOnCollectionResponse.prettyPrint(); + assertEquals(200, grantCuratorOnCollectionResponse.getStatusCode()); + + // A number of roles should now be assignable + Response listUser2AssignableRolesResponseWithManagePermission2 = UtilIT.getUserAssignableRolesForDataset(datasetId2, apiToken2); + listUser2AssignableRolesResponseWithManagePermission2.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(4)) .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) - .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + .body("data[1].alias", equalTo(DataverseRole.EDITOR)) + .body("data[2].alias", equalTo(DataverseRole.CURATOR)) + .body("data[3].alias", equalTo(DataverseRole.MEMBER)); // Clean up Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); @@ -7645,6 +7695,9 @@ public void testListAssignableRoles() { Response deleteUser2Response = UtilIT.deleteUser(username2); assertEquals(200, deleteUser2Response.getStatusCode()); + + Response deleteUser3Response = UtilIT.deleteUser(username3); + assertEquals(200, deleteUser3Response.getStatusCode()); } private String getSuperuserToken() { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 2cddede825a..7e71bf77215 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -3306,25 +3306,57 @@ public void testListAssignableRoles() { // List user-assignable roles for second user Response listUser2AssignableRolesResponse = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); listUser2AssignableRolesResponse.prettyPrint(); + // No roles should be user-assignable, since the requesting user has no permissions for the collection listUser2AssignableRolesResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.size()", equalTo(0)); // Assign role to second user for the collection - Response grantRoleResponse = UtilIT.grantRoleOnDataverse(dvAlias, "member", "@" + username2, apiToken); + Response grantRoleResponse = UtilIT.grantRoleOnDataverse(dvAlias, DataverseRole.MEMBER, "@" + username2, apiToken); grantRoleResponse.prettyPrint(); assertEquals(200, grantRoleResponse.getStatusCode()); // List user-assignable roles for second user again Response listUser2AssignableRolesResponse2 = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); listUser2AssignableRolesResponse2.prettyPrint(); - // Now two roles should be user-assignable, since the requesting user has received some permissions for the collection + // Still, no roles should be user-assignable, since the requesting user has no ManageDataversePermissions permission listUser2AssignableRolesResponse2.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.size()", equalTo(2)) - .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) - .body("data[1].alias", equalTo(DataverseRole.MEMBER)); + .body("data.size()", equalTo(0)); + + // Grant a role which does not include ManageDataversePermissions + Response grantCuratorOnCollectionResponse = UtilIT.grantRoleOnDataverse(dvAlias, DataverseRole.CURATOR, "@" + username2, apiToken); + grantCuratorOnCollectionResponse.prettyPrint(); + assertEquals(200, grantCuratorOnCollectionResponse.getStatusCode()); + + // List user-assignable roles for second user again + Response listUser2AssignableRolesResponse3 = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); + listUser2AssignableRolesResponse3.prettyPrint(); + // Still, no roles should be user-assignable, since the requesting user has no ManageDataversePermissions permission + listUser2AssignableRolesResponse3.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(0)); + + // Grant a role which includes ManageDataversePermissions + Response grantAdminOnCollectionResponse = UtilIT.grantRoleOnDataverse(dvAlias, DataverseRole.ADMIN, "@" + username2, apiToken); + grantAdminOnCollectionResponse.prettyPrint(); + assertEquals(200, grantAdminOnCollectionResponse.getStatusCode()); + + // All available roles should now be assignable + Response listUser2AssignableRolesResponseWithManagePermission = UtilIT.getUserAssignableRolesForDataverse(dvAlias, apiToken2); + listUser2AssignableRolesResponseWithManagePermission.prettyPrint(); + listUser2AssignableRolesResponseWithManagePermission.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(8)) + .body("data[0].alias", equalTo(DataverseRole.ADMIN)) + .body("data[1].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[2].alias", equalTo(DataverseRole.FULL_CONTRIBUTOR)) + .body("data[3].alias", equalTo(DataverseRole.DV_CONTRIBUTOR)) + .body("data[4].alias", equalTo(DataverseRole.DS_CONTRIBUTOR)) + .body("data[5].alias", equalTo(DataverseRole.EDITOR)) + .body("data[6].alias", equalTo(DataverseRole.CURATOR)) + .body("data[7].alias", equalTo(DataverseRole.MEMBER)); // Clean up Response deleteDataverseResponse = UtilIT.deleteDataverse(dvAlias, apiToken); From 1b6440092246d0309b25ada0391d0a84927607af Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Wed, 9 Sep 2026 13:43:23 +0200 Subject: [PATCH 10/10] fix: update role filter logic and tests for user-assignable roles to check using request context instead of only user --- .../dataverse/DataverseRoleServiceBean.java | 6 +-- .../harvard/iq/dataverse/api/Datasets.java | 2 +- .../harvard/iq/dataverse/api/Dataverses.java | 2 +- .../harvard/iq/dataverse/api/DatasetsIT.java | 54 ++++++++++++++++++- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index a0d77523794..a06f34ccc8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -397,11 +397,11 @@ public Set availableRoles(DvObject dvo) { * Currently, the available roles for Datasets and DataFiles are gotten from the collection they are in. * * @param dvo The Dataset, DataFile or Dataverse whose available roles we query - * @param user The user whose available roles we query + * @param request The request whose user and context are used to query available roles * @return Set of available roles */ - public Set availableRoles(DvObject dvo, User user) { - Set granted = permissionService.permissionsFor(user, dvo); + public Set availableRoles(DvObject dvo, DataverseRequest request) { + Set granted = permissionService.permissionsFor(request, dvo); Permission managePermission = dvo instanceof Dataverse ? Permission.ManageDataversePermissions diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7a49fdf605d..d8fe3622d07 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2470,7 +2470,7 @@ public Response getAssignments(@Context ContainerRequestContext crc, @Parameter( description = "List of user-assignable roles", content = @Content(mediaType = "application/json")) public Response getAssignableRoles(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String id) { - return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDatasetOrDie(id), req.getUser())))), getRequestUser(crc)); + return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDatasetOrDie(id), req)))), getRequestUser(crc)); } @GET diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 0f9539d6f96..b5527c7bfb0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1583,7 +1583,7 @@ public Response listAssignments(@Context ContainerRequestContext crc, description = "List of user-assignable roles", content = @Content(mediaType = "application/json")) public Response getAssignableRoles(@Context ContainerRequestContext crc, @Parameter(description = "Dataverse alias, id, or persistent identifier.", required = true) @PathParam("identifier") String dvIdtf) { - return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDataverseOrDie(dvIdtf), req.getUser())))), getRequestUser(crc)); + return response(req -> ok(jsonDataverseRoles(new ArrayList<>(dataverseRoleService.availableRoles(findDataverseOrDie(dvIdtf), req)))), getRequestUser(crc)); } @POST diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 35c210c71f4..830a408c8a8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7632,7 +7632,8 @@ public void testListAssignableRoles() { Response assignReturnedRoleResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, returnedRole, "@" + username3, apiToken2); assertEquals(200, assignReturnedRoleResponse.getStatusCode()); - // Now, we'll test user-assignable roles coming from roles assigned at the collection level + // Now, we'll test user-assignable roles coming from roles assigned at the collection level instead of + // at the dataset level // Create a second collection (under root) Response createDataverse2Response = UtilIT.createRandomDataverse(apiToken); String dvAlias2 = UtilIT.getAliasFromResponse(createDataverse2Response); @@ -7677,6 +7678,48 @@ public void testListAssignableRoles() { .body("data[2].alias", equalTo(DataverseRole.CURATOR)) .body("data[3].alias", equalTo(DataverseRole.MEMBER)); + // Now, we'll test user-assignable roles coming from roles assigned based on IP group + // Create a third collection (under root) + Response createDataverse3Response = UtilIT.createRandomDataverse(apiToken); + String dvAlias3 = UtilIT.getAliasFromResponse(createDataverse3Response); + + // Create dataset in that collection + Response createDataset3Response = UtilIT.createRandomDatasetViaNativeApi(dvAlias3, apiToken); + Integer datasetId3 = UtilIT.getDatasetIdFromResponse(createDataset3Response); + String dataset3PersistentId = UtilIT.getDatasetPersistentIdFromResponse(createDataset3Response); + + // Before IP-group based assignment is created, no roles should be user-assignable + UtilIT.getUserAssignableRolesForDataset(Integer.valueOf(datasetId3), apiToken2) + .then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(0)); + + // Assign curator role on dataset based on IP group + String ipGroupAlias = "assignableRolesIpGroup" + UtilIT.getRandomIdentifier(); + JsonObjectBuilder ipGroupJson = JsonUtil.createObjectBuilder() + .add("alias", ipGroupAlias) + .add("name", "IP group for assignable roles integration test") + .add("ranges", JsonUtil.createArrayBuilder() + .add(JsonUtil.createArrayBuilder().add("0.0.0.0").add("255.255.255.255"))); + Response createIpGroupResponse = UtilIT.createIpGroup(ipGroupJson.build()); + assertEquals(CREATED.getStatusCode(), createIpGroupResponse.getStatusCode()); + + String ipGroupIdentifier = "&ip/" + ipGroupAlias; + Response grantRoleBasedOnIpGroupResponse = UtilIT.grantRoleOnDataset( + dataset3PersistentId, DataverseRole.CURATOR, ipGroupIdentifier, apiToken); + assertEquals(OK.getStatusCode(), grantRoleBasedOnIpGroupResponse.getStatusCode()); + + // A number of roles should now be assignable + Response listUser2AssignableRolesResponseWithIpGroupPermission = UtilIT.getUserAssignableRolesForDataset( + Integer.valueOf(datasetId3), apiToken2); + listUser2AssignableRolesResponseWithIpGroupPermission.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.size()", equalTo(4)) + .body("data[0].alias", equalTo(DataverseRole.FILE_DOWNLOADER)) + .body("data[1].alias", equalTo(DataverseRole.EDITOR)) + .body("data[2].alias", equalTo(DataverseRole.CURATOR)) + .body("data[3].alias", equalTo(DataverseRole.MEMBER)); + // Clean up Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); assertEquals(200, destroyDatasetResponse.getStatusCode()); @@ -7684,12 +7727,18 @@ public void testListAssignableRoles() { Response destroyDataset2Response = UtilIT.destroyDataset(datasetId2, apiToken); assertEquals(200, destroyDataset2Response.getStatusCode()); + Response destroyDataset3Response = UtilIT.destroyDataset(datasetId3, apiToken); + assertEquals(200, destroyDataset3Response.getStatusCode()); + Response deleteDataverseResponse = UtilIT.deleteDataverse(dvAlias, apiToken); assertEquals(200, deleteDataverseResponse.getStatusCode()); Response deleteDataverse2Response = UtilIT.deleteDataverse(dvAlias2, apiToken); assertEquals(200, deleteDataverse2Response.getStatusCode()); + Response deleteDataverse3Response = UtilIT.deleteDataverse(dvAlias3, apiToken); + assertEquals(200, deleteDataverse3Response.getStatusCode()); + Response deleteUserResponse = UtilIT.deleteUser(username); assertEquals(200, deleteUserResponse.getStatusCode()); @@ -7698,6 +7747,9 @@ public void testListAssignableRoles() { Response deleteUser3Response = UtilIT.deleteUser(username3); assertEquals(200, deleteUser3Response.getStatusCode()); + + Response deleteIpGroupResponse = UtilIT.deleteIpGroup(ipGroupAlias); + assertEquals(200, deleteIpGroupResponse.getStatusCode()); } private String getSuperuserToken() {