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 582891e0861..721ad91a3af 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -529,6 +529,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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -3122,6 +3143,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-dataset-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: diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index f16726a8fbe..a06f34ccc8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -54,6 +54,8 @@ public class DataverseRoleServiceBean implements java.io.Serializable { @EJB SolrIndexServiceBean solrIndexService; @EJB + PermissionServiceBean permissionService; + @EJB IndexAsync indexAsync; public DataverseRole save(DataverseRole aRole) { @@ -87,11 +89,11 @@ public RoleAssignment save(RoleAssignment assignment, boolean createIndex, Datav } else { assignment = em.merge(assignment); } - + if (createIndex) { indexAsync.indexRole(assignment); } - + // Check if ROLE_ASSIGNMENT_HISTORY feature flag is enabled if (FeatureFlags.ROLE_ASSIGNMENT_HISTORY.enabled()) { RoleAssignmentHistory entry = new RoleAssignmentHistory(assignment, req, RoleAssignmentHistory.ActionType.ASSIGN); @@ -100,11 +102,11 @@ public RoleAssignment save(RoleAssignment assignment, boolean createIndex, Datav return assignment; } - + /** * Saves a RoleAssignmentHistory entry to the database. - * + * * @param entry The RoleAssignmentHistory object to be saved * @return The persisted RoleAssignmentHistory object */ @@ -181,42 +183,42 @@ public void revoke(RoleAssignment ra, DataverseRequest req) { if (!em.contains(ra)) { ra = em.merge(ra); } - + // Create history entry if feature flag is set if (FeatureFlags.ROLE_ASSIGNMENT_HISTORY.enabled()) { RoleAssignmentHistory entry = new RoleAssignmentHistory(ra, req, RoleAssignmentHistory.ActionType.REVOKE); saveHistoryEntry(entry); } - + em.remove(ra); /** * @todo update permissionModificationTime here. */ indexAsync.indexRole(ra); } - + // "nuclear" remove-all roles for a user or group: // (Note that all the "definition points" - i.e., the dvObjects // on which the roles were assigned - need to be reindexed for permissions // once the role assignments are removed! public void revokeAll(RoleAssignee assignee, DataverseRequest req) { Set reindexSet = new HashSet<>(); - + for (RoleAssignment ra : roleAssigneeService.getAssignmentsFor(assignee.getIdentifier())) { if (!em.contains(ra)) { ra = em.merge(ra); } - + // Create history entry if feature flag is set if (FeatureFlags.ROLE_ASSIGNMENT_HISTORY.enabled()) { RoleAssignmentHistory entry = new RoleAssignmentHistory(ra, req, RoleAssignmentHistory.ActionType.REVOKE); saveHistoryEntry(entry); } - + em.remove(ra); reindexSet.add(ra.getDefinitionPoint()); } - + indexAsync.indexRoles(reindexSet); } @@ -337,12 +339,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); @@ -357,6 +357,67 @@ 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 request The request whose user and context are used to query available roles + * @return Set of available roles + */ + public Set availableRoles(DvObject dvo, DataverseRequest request) { + Set granted = permissionService.permissionsFor(request, dvo); + + 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) { /* For a given permission and dataverse Id get all of the roles (built-in or owned by the dataverse) @@ -372,10 +433,10 @@ For a given permission and dataverse Id get all of the roles (built-in or owned } return retVal; } - + /** * Retrieves role assignment history for a specific definition point - * + * * @param definitionPointId The ID of the definition point * @return List of role assignment history entries */ @@ -383,13 +444,13 @@ public List getRoleAssignmentHistory(Lon List entries = em.createNamedQuery("RoleAssignmentHistory.findByDefinitionPointId", RoleAssignmentHistory.class) .setParameter("definitionPointId", definitionPointId) .getResultList(); - + return processRoleAssignmentEntries(entries, false); } /** * Retrieves role assignment history for all files in a dataset - * + * * @param datasetId The ID of the dataset * @return List of role assignment history entries */ @@ -397,13 +458,13 @@ public List getFilesRoleAssignmentHistor List entries = em.createNamedQuery("RoleAssignmentHistory.findByOwnerId", RoleAssignmentHistory.class) .setParameter("datasetId", datasetId) .getResultList(); - + return processRoleAssignmentEntries(entries, true); } - + /** * Common method to process role assignment history entries and create consolidated history entries - * + * * @param entries List of role assignment history records * @param combineEntries Whether to combine entries for different files * @return List of role assignment history entries @@ -430,7 +491,7 @@ private List processRoleAssignmentEntrie consolidatedEntry.setRevokedAt(entry.getActionTimestamp()); } } - + // Second pass: Combine entries with matching criteria if requested if (combineEntries) { Map finalHistoryMap = new HashMap<>(); @@ -470,62 +531,62 @@ public static class RoleAssignmentHistoryConsolidatedEntry { private String revokedBy; private Date revokedAt; private List definitionPointIds; - + public RoleAssignmentHistoryConsolidatedEntry(String assigneeIdentifier, String roleName, Long definitionPointId) { this.roleName = roleName; this.assigneeIdentifier = assigneeIdentifier; this.definitionPointIds = new ArrayList(); definitionPointIds.add(definitionPointId); } - + public void setRevokedAt(Date actionTimestamp) { revokedAt = actionTimestamp; } - + public void setRevokedBy(String actionByIdentifier) { revokedBy = actionByIdentifier; } - + public void setAssignedAt(Date actionTimestamp) { assignedAt = actionTimestamp; } - + public void setAssignedBy(String actionByIdentifier) { assignedBy = actionByIdentifier; } - + public String getRoleName() { return roleName; } - + public String getAssigneeIdentifier() { return assigneeIdentifier; } - + public String getAssignedBy() { return assignedBy; } - + public Date getAssignedAt() { return assignedAt; } - + public String getRevokedBy() { return revokedBy; } - + public Date getRevokedAt() { return revokedAt; } - + public List getDefinitionPointIds() { return definitionPointIds; } - + public void addDefinitionPointId(Long definitionPointId) { definitionPointIds.add(definitionPointId); } - + public String getDefinitionPointIdsAsString() { return definitionPointIds.stream() .map(Object::toString) diff --git a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java index f5cd859e7ac..67024a6339b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java @@ -39,6 +39,7 @@ 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; @@ -239,18 +240,18 @@ public void cloneRole(String roleId) { public void editRole(String roleId) { setRole(roleService.find(Long.parseLong(roleId))); } - + /** Role Assignment History */ private List roleAssignmentHistory; public List getRoleAssignmentHistory() { - + if (roleAssignmentHistory == null) { roleAssignmentHistory = roleService.getRoleAssignmentHistory(dvObject.getId()); } return roleAssignmentHistory; } - + /* ============================================================================ @@ -423,40 +424,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() { @@ -699,7 +673,7 @@ public Boolean getRenderRoleMessages() { public void setRenderRoleMessages(Boolean renderRoleMessages) { this.renderRoleMessages = renderRoleMessages; } - + public String getSignedUrlForRAHistoryCsv() { String apiPath; //Including /v1 in these urls is required for the signature to validate @@ -713,17 +687,17 @@ public String getSignedUrlForRAHistoryCsv() { // For other types (like DataFile), return null or a default path return null; } - + try { // Get the application URL from the system config String baseUrl = SystemConfig.getDataverseSiteUrlStatic(); if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } - + // Construct the full URL String fullApiPath = baseUrl + apiPath; - + // Generate a signed URL with the user's API token User user = session.getUser(); String key = null; @@ -731,7 +705,7 @@ public String getSignedUrlForRAHistoryCsv() { if (user instanceof AuthenticatedUser authUser) { userId = authUser.getUserIdentifier(); ApiToken apiToken = authenticationService.findApiTokenByUser(authUser); - + if (apiToken != null && !apiToken.isExpired() && !apiToken.isDisabled()) { key = apiToken.getTokenString(); } @@ -746,7 +720,7 @@ public String getSignedUrlForRAHistoryCsv() { } return null; } - + public String getPermissionsHistoryFilename() { if (dvObject instanceof Dataverse dv) { return dv.getAlias() + "_permissions_history.csv"; 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 1061eae6f30..d8fe3622d07 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2461,6 +2461,18 @@ public Response getAssignments(@Context ContainerRequestContext crc, @Parameter( .stream().map(ra -> json(ra)).collect(toJsonArray())), getRequestUser(crc)); } + @GET + @AuthRequired + @Path("{identifier}/assignments/userAssignableRoles") + @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)))), 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 28a67237af1..b5527c7bfb0 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; @@ -131,7 +133,10 @@ public class Dataverses extends AbstractApiBean { @EJB DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; - + + @EJB + DataverseRoleServiceBean dataverseRoleService; + @POST @AuthRequired @Operation(summary = "Create the root dataverse", @@ -1569,6 +1574,18 @@ public Response listAssignments(@Context ContainerRequestContext crc, ), getRequestUser(crc)); } + @GET + @AuthRequired + @Path("{identifier}/assignments/userAssignableRoles") + @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)))), getRequestUser(crc)); + } + @POST @AuthRequired @Path("{identifier}/assignments") 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 d6968546694..830a408c8a8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3088,7 +3088,7 @@ public void testDatasetLocksApi() { getDatasetJson.then().assertThat() .body("data.locks[0]", equalTo("Ingest")) .statusCode(200); - + // Try to lock the dataset with the same type lock, AGAIN // (this should fail, of course!) lockDatasetResponse = UtilIT.lockDataset(datasetId.longValue(), "Ingest", apiToken); @@ -4037,7 +4037,7 @@ private JsonObject getDataAsJsonObject(String body) { private String getData(String body) { return getDataAsJsonObject(body).toString(); } - + @Test public void testFilesUnchangedAfterDatasetMetadataUpdate() throws IOException { Response createUser = UtilIT.createRandomUser(); @@ -4168,7 +4168,7 @@ public void testUpdateDatasetTermsOfAccess() throws IOException { .body("data.termsOfAccess", equalTo("For access to restricted files please see read me file")) .body("data.dataAccessPlace", equalTo("dataAccessPlace")) .body("data.license.name", equalTo("CC0 1.0")); - + // Restrict file Response restrictFileResponse = UtilIT.restrictFile(fileId, true, apiToken); @@ -4197,7 +4197,7 @@ public void testUpdateDatasetTermsOfAccess() throws IOException { updateTerms.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.fileAccessRequest", equalTo(false)); - + String badPID = "QQQAndABatmanSymbol"; updateTerms = UtilIT.updateDatasetTermsAndAccess(badPID, apiToken, pathToJsonFile); updateTerms.prettyPrint(); @@ -4205,17 +4205,17 @@ public void testUpdateDatasetTermsOfAccess() throws IOException { updateTerms.then().assertThat() .statusCode(BAD_REQUEST.getStatusCode()) .body("message", containsString("QQQ")); - + //Make installation "public install" tp false to see that terms of access can be set UtilIT.setSetting(SettingsServiceBean.Key.PublicInstall, "false"); - + createDataset = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); createDataset.prettyPrint(); createDataset.then().assertThat() .statusCode(CREATED.getStatusCode()); datasetPersistentId = JsonPath.from(createDataset.body().asString()).getString("data.persistentId"); int datasetId3 = JsonPath.from(createDataset.body().asString()).getInt("data.id"); - + pathToJsonFile = "src/test/resources/json/update-dataset-access-only.json"; updateTerms = UtilIT.updateDatasetTermsAndAccess(datasetPersistentId, apiToken, pathToJsonFile); updateTerms.prettyPrint(); @@ -4225,21 +4225,21 @@ public void testUpdateDatasetTermsOfAccess() throws IOException { //reset public install UtilIT.setSetting(SettingsServiceBean.Key.PublicInstall, publicInstall); - + String username = UtilIT.getUsernameFromResponse(createUser); //for cleanup Response makeSuperUser = UtilIT.setSuperuserStatus(username, true); - + // Clean up Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); destroyDatasetResponse.prettyPrint(); assertEquals(200, destroyDatasetResponse.getStatusCode()); - + destroyDatasetResponse = UtilIT.destroyDataset(datasetId2, apiToken); destroyDatasetResponse.prettyPrint(); assertEquals(200, destroyDatasetResponse.getStatusCode()); - + destroyDatasetResponse = UtilIT.destroyDataset(datasetId3, apiToken); destroyDatasetResponse.prettyPrint(); assertEquals(200, destroyDatasetResponse.getStatusCode()); @@ -4247,10 +4247,10 @@ public void testUpdateDatasetTermsOfAccess() throws IOException { Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); deleteDataverseResponse.prettyPrint(); assertEquals(200, deleteDataverseResponse.getStatusCode()); - + Response deleteUserResponse = UtilIT.deleteUser(username); assertEquals(200, deleteUserResponse.getStatusCode()); - + } @@ -7540,6 +7540,218 @@ public void testGetDatasetWithTermsOfUseAndGuestbook() throws IOException, JsonP .body("data.guestbookId", equalTo(guestbook.getId().intValue())); } + @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, 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(); + // No roles should be user-assignable, since the requesting user has no ManageDatasetPermissions permission + listUser2AssignableRolesResponse2.then().assertThat() + .statusCode(OK.getStatusCode()) + .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.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 instead of + // at the dataset level + // 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, 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(); + // No roles should be user-assignable, since the requesting user has no ManageDatasetPermissions permission + listUser2AssignableRolesResponse4.then().assertThat() + .statusCode(OK.getStatusCode()) + .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.EDITOR)) + .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()); + + 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()); + + Response deleteUser2Response = UtilIT.deleteUser(username2); + assertEquals(200, deleteUser2Response.getStatusCode()); + + Response deleteUser3Response = UtilIT.deleteUser(username3); + assertEquals(200, deleteUser3Response.getStatusCode()); + + Response deleteIpGroupResponse = UtilIT.deleteIpGroup(ipGroupAlias); + assertEquals(200, deleteIpGroupResponse.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 7cc05c48b2a..7e71bf77215 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -3272,6 +3272,103 @@ 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, 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(); + // 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(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); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); + + Response deleteUser2Response = UtilIT.deleteUser(username2); + assertEquals(200, deleteUser2Response.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 fdb1b4f53f1..5868c184451 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2816,6 +2816,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; @@ -2840,6 +2846,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 = JsonUtil.createObjectBuilder();