diff --git a/doc/release-notes/8174-new-managefilepermissions.md b/doc/release-notes/8174-new-managefilepermissions.md new file mode 100644 index 00000000000..81ef0821d69 --- /dev/null +++ b/doc/release-notes/8174-new-managefilepermissions.md @@ -0,0 +1,3 @@ +## New ManageFilePermissions Permission + +Dataverse can now support a use case in which a Admin or Curator would like to delegate the ability to grant access to restricted files to other users. This can be implemented by creating a custom role (e.g. DownloadApprover) that has the new ManageFilePermissions permission. This release introduces the new permission ( and adjusts the existing standard Admin and Curator roles so they continue to have the ability to grant file download requrests). \ No newline at end of file diff --git a/scripts/api/data/role-curator.json b/scripts/api/data/role-curator.json index 2de5b2aefd1..91cb7ec43e2 100644 --- a/scripts/api/data/role-curator.json +++ b/scripts/api/data/role-curator.json @@ -9,6 +9,7 @@ "DeleteDatasetDraft", "PublishDataset", "ManageDatasetPermissions", + "ManageFilePermissions", "AddDataverse", "AddDataset", "ViewUnpublishedDataverse" diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java index 6b8d0f73b83..d1314ae5f5b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java @@ -315,7 +315,7 @@ public void requestAccessMultiple(List files) { } } if (notificationFile != null && succeeded) { - fileDownloadService.sendRequestFileAccessNotification(notificationFile.getOwner(), notificationFile.getId(), (AuthenticatedUser) session.getUser()); + fileDownloadService.sendRequestFileAccessNotification(notificationFile, (AuthenticatedUser) session.getUser()); } } @@ -339,7 +339,7 @@ private boolean processRequestAccess(DataFile file, Boolean sendNotification) { file.getFileAccessRequesters().add((AuthenticatedUser) session.getUser()); // create notification if necessary if (sendNotification) { - fileDownloadService.sendRequestFileAccessNotification(file.getOwner(), file.getId(), (AuthenticatedUser) session.getUser()); + fileDownloadService.sendRequestFileAccessNotification(file, (AuthenticatedUser) session.getUser()); } return true; } diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java index 22bfc191921..d64f3fba808 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java @@ -500,9 +500,9 @@ public boolean requestAccess(Long fileId) { return false; } - public void sendRequestFileAccessNotification(Dataset dataset, Long fileId, AuthenticatedUser requestor) { - permissionService.getUsersWithPermissionOn(Permission.ManageDatasetPermissions, dataset).stream().forEach((au) -> { - userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.REQUESTFILEACCESS, fileId, null, requestor, false); + public void sendRequestFileAccessNotification(DataFile datafile, AuthenticatedUser requestor) { + permissionService.getUsersWithPermissionOn(Permission.ManageFilePermissions, datafile).stream().forEach((au) -> { + userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.REQUESTFILEACCESS, datafile.getId(), null, requestor, false); }); } diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java index c728062a5a8..09f067f772c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java @@ -136,7 +136,7 @@ public String init() { return permissionsWrapper.notFound(); } - if (!permissionService.on(dataset).has(Permission.ManageDatasetPermissions)) { + if (!permissionService.on(dataset).has(Permission.ManageFilePermissions)) { return permissionsWrapper.notAuthorized(); } initMaps(); diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 92892a7fe77..d255d2ec394 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -142,7 +142,9 @@ public boolean canUpdateDataset(DataverseRequest dr, Dataset dataset) { return doesSessionUserHaveDataSetPermission(dr, dataset, Permission.EditDataset); } - + public boolean canManageFilesOnDataset(Dataset dataset) { + return doesSessionUserHaveDataSetPermission(dvRequestService.getDataverseRequest(), dataset, Permission.ManageFilePermissions); + } /** * (Using Raman's implementation in DatasetPage - moving it here, so that diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 081c2105b1a..d171658fe2d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -121,6 +121,7 @@ import javax.ws.rs.RedirectionException; import javax.ws.rs.core.MediaType; import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; import org.glassfish.jersey.media.multipart.FormDataParam; /* @@ -1367,21 +1368,20 @@ public Response listFileAccessRequests(@PathParam("id") String fileToRequestAcce } try { - dataverseRequest = createDataverseRequest(findUserOrDie()); + dataverseRequest = createDataverseRequest(findAuthenticatedUserOrDie()); } catch (WrappedResponse wr) { List args = Arrays.asList(wr.getLocalizedMessage()); - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", args)); + return error(UNAUTHORIZED, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", args)); } - - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageDatasetPermissions))) { - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); + if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { + return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } List requesters = dataFile.getFileAccessRequesters(); if (requesters == null || requesters.isEmpty()) { List args = Arrays.asList(dataFile.getDisplayName()); - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestList.noRequestsFound")); + return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestList.noRequestsFound", args)); } JsonArrayBuilder userArray = Json.createArrayBuilder(); @@ -1567,7 +1567,7 @@ public Response rejectFileAccess(@PathParam("id") String fileToRequestAccessId, return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", args)); } - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageDatasetPermissions))) { + if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java b/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java index 7fd7a40587f..32937098118 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java @@ -46,11 +46,13 @@ public enum Permission implements java.io.Serializable { EditDataset(BundleUtil.getStringFromBundle("permission.editDataset"), true, Dataset.class), ManageDataversePermissions(BundleUtil.getStringFromBundle("permission.managePermissionsDataverse"), true, Dataverse.class), ManageDatasetPermissions(BundleUtil.getStringFromBundle("permission.managePermissionsDataset"), true, Dataset.class), + ManageFilePermissions(BundleUtil.getStringFromBundle("permission.managePermissionsDataFile"), true, DataFile.class), PublishDataverse(BundleUtil.getStringFromBundle("permission.publishDataverse"), true, Dataverse.class), PublishDataset(BundleUtil.getStringFromBundle("permission.publishDataset"), true, Dataset.class, Dataverse.class), // Delete DeleteDataverse(BundleUtil.getStringFromBundle("permission.deleteDataverse"), true, Dataverse.class), DeleteDatasetDraft(BundleUtil.getStringFromBundle("permission.deleteDataset"), true, Dataset.class); + // FUTURE: //RestrictMetadata("Mark metadata as restricted", DvObject.class), diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java index b2af28befb5..5577d541012 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java @@ -32,6 +32,7 @@ public class AssignRoleCommand extends AbstractCommand { private final DataverseRole role; private final RoleAssignee grantee; + //Kept for convenience -could get this as the only DVObject AbstractCommand<>.getAffectedDvObjects() instead of having a local defPoint private final DvObject defPoint; private final String privateUrlToken; private boolean anonymizedAccess; @@ -45,7 +46,7 @@ public class AssignRoleCommand extends AbstractCommand { */ public AssignRoleCommand(RoleAssignee anAssignee, DataverseRole aRole, DvObject assignmentPoint, DataverseRequest aRequest, String privateUrlToken) { // for data file check permission on owning dataset - super(aRequest, assignmentPoint instanceof DataFile ? assignmentPoint.getOwner() : assignmentPoint); + super(aRequest, assignmentPoint); role = aRole; grantee = anAssignee; defPoint = assignmentPoint; @@ -76,7 +77,7 @@ public Map> getRequiredPermissions() { // for data file check permission on owning dataset return Collections.singletonMap("", defPoint instanceof Dataverse ? Collections.singleton(Permission.ManageDataversePermissions) - : Collections.singleton(Permission.ManageDatasetPermissions)); + : defPoint instanceof Dataset ? Collections.singleton(Permission.ManageDatasetPermissions) : Collections.singleton(Permission.ManageFilePermissions)); } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestAccessCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestAccessCommand.java index 0a1d41ff49c..2fc3a5c525e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestAccessCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestAccessCommand.java @@ -61,7 +61,7 @@ public DataFile execute(CommandContext ctxt) throws CommandException { } file.getFileAccessRequesters().add(requester); if (sendNotification) { - ctxt.fileDownload().sendRequestFileAccessNotification(this.file.getOwner(), this.file.getId(), requester); + ctxt.fileDownload().sendRequestFileAccessNotification(this.file, requester); } return ctxt.files().save(file); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RevokeRoleCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RevokeRoleCommand.java index ca8165c3ed7..26ab88d29d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RevokeRoleCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RevokeRoleCommand.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.RoleAssignment; @@ -23,8 +23,7 @@ public class RevokeRoleCommand extends AbstractVoidCommand { private final RoleAssignment toBeRevoked; public RevokeRoleCommand(RoleAssignment toBeRevoked, DataverseRequest aRequest) { - // for data file check permission on owning dataset - super(aRequest, toBeRevoked.getDefinitionPoint() instanceof DataFile ? toBeRevoked.getDefinitionPoint().getOwner() : toBeRevoked.getDefinitionPoint()); + super(aRequest, toBeRevoked.getDefinitionPoint()); this.toBeRevoked = toBeRevoked; } @@ -35,10 +34,10 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { @Override public Map> getRequiredPermissions() { - // for data file check permission on owning dataset + DvObject defPoint = toBeRevoked.getDefinitionPoint(); return Collections.singletonMap("", - toBeRevoked.getDefinitionPoint() instanceof Dataverse ? Collections.singleton(Permission.ManageDataversePermissions) - : Collections.singleton(Permission.ManageDatasetPermissions)); + defPoint instanceof Dataverse ? Collections.singleton(Permission.ManageDataversePermissions) + : defPoint instanceof Dataset ? Collections.singleton(Permission.ManageDatasetPermissions): Collections.singleton(Permission.ManageFilePermissions)); } @Override public String describe() { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a1768b2a32c..a06d2d2d67b 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2208,6 +2208,7 @@ permission.deleteDataset=Delete a dataset draft permission.deleteDataverse=Delete an unpublished dataverse permission.publishDataset=Publish a dataset permission.publishDataverse=Publish a dataverse +permission.managePermissionsDataFile=Manage permissions for a file permission.managePermissionsDataset=Manage permissions for a dataset permission.managePermissionsDataverse=Manage permissions for a dataverse permission.editDataset=Edit a dataset's metadata @@ -2499,7 +2500,7 @@ access.api.revokeAccess.noRoleFound=No File Downloader role found for user {0} access.api.revokeAccess.success.for.single.file=File Downloader access has been revoked for user {0} on file {1} access.api.requestList.fileNotFound=Could not find datafile with id {0}. access.api.requestList.noKey=You must provide a key to get list of access requests for a file. -access.api.requestList.noRequestsFound=There are no access requests for this file {0}. +access.api.requestList.noRequestsFound=There are no access requests for this file: {0}. access.api.exception.metadata.not.available.for.nontabular.file=This type of metadata is only available for tabular files. access.api.exception.metadata.restricted.no.permission=You do not have permission to download this file. access.api.exception.version.not.found=Could not find requested dataset version. @@ -2519,12 +2520,14 @@ permission.PublishDataverse.label=PublishDataverse permission.PublishDataset.label=PublishDataset permission.DeleteDataverse.label=DeleteDataverse permission.DeleteDatasetDraft.label=DeleteDatasetDraft +permission.ManageFilePermissions.label=ManageFilePermissions permission.AddDataverse.desc=Add a dataverse within another dataverse permission.DeleteDatasetDraft.desc=Delete a dataset draft permission.DeleteDataverse.desc=Delete an unpublished dataverse permission.PublishDataset.desc=Publish a dataset permission.PublishDataverse.desc=Publish a dataverse +permission.ManageFilePermissions.desc=Manage permissions for a file permission.ManageDatasetPermissions.desc=Manage permissions for a dataset permission.ManageDataversePermissions.desc=Manage permissions for a dataverse permission.EditDataset.desc=Edit a dataset's metadata diff --git a/src/main/resources/db/migration/V5.8.0.1__8109-add-manage-files-permission.sql b/src/main/resources/db/migration/V5.8.0.1__8109-add-manage-files-permission.sql new file mode 100644 index 00000000000..2fae7f4d8df --- /dev/null +++ b/src/main/resources/db/migration/V5.8.0.1__8109-add-manage-files-permission.sql @@ -0,0 +1,25 @@ +/* We're adding a new permission at bit 10 (512 ManageFilePermissions) that should be set for any role that has the permission + in bit 9 (256 ManageDatasetPermissions). +That means that any roles with current bits 10-13 (512+1024+2048+4096 = 7680) needs to have the corresponding bits 11-14 set +after the update, which can be accomplished by adding permissionbits & 7680 to the original value. (So a role with just bit 10 +set (512) would have 512&7680 (=512) added to it and become bit 11 (1024). The same logic also shifts any values in bits 11-13 +into bits 12-14. + +In addition, any role wite permission for bit 9 set now should also have bit 10 set after the update +(any current role with ManageDatasetPermissions gets ManageDatasetPermissions and ManageFilePermissions after the update). +To do this we just add an addition bit 10 (512) if permissionbits&256!=0 + +Finally, to make this idempotent, at least under the assumption that the standard admin role with all permissions +(or some role with current permission bit 13 set), we check to make sure no role has bit 14+ set - max(permissionsbits) <8192. +With the IF statement, running this script more than once will only cause an update on the first pass. + +*/ +DO +$do$ +BEGIN + IF (SELECT MAX(permissionbits) FROM dataverserole) < 8192 THEN + UPDATE dataverserole SET permissionbits=permissionbits + (permissionbits & 7680) WHERE (permissionbits & 256 =0); + UPDATE dataverserole SET permissionbits=permissionbits + (permissionbits & 7680) +512 WHERE (permissionbits & 256 !=0); + END IF; +END +$do$; diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 3f3f7574a73..7d3f435aedb 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -384,18 +384,18 @@ - +
  • #{bundle['dataset.editBtn.itemLabel.permissions']}