Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/release-notes/8174-new-managefilepermissions.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions scripts/api/data/role-curator.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"DeleteDatasetDraft",
"PublishDataset",
"ManageDatasetPermissions",
"ManageFilePermissions",
"AddDataverse",
"AddDataset",
"ViewUnpublishedDataverse"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ public void requestAccessMultiple(List<DataFile> files) {
}
}
if (notificationFile != null && succeeded) {
fileDownloadService.sendRequestFileAccessNotification(notificationFile.getOwner(), notificationFile.getId(), (AuthenticatedUser) session.getUser());
fileDownloadService.sendRequestFileAccessNotification(notificationFile, (AuthenticatedUser) session.getUser());
}
}

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions src/main/java/edu/harvard/iq/dataverse/api/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
Expand Down Expand Up @@ -1367,21 +1368,20 @@ public Response listFileAccessRequests(@PathParam("id") String fileToRequestAcce
}

try {
dataverseRequest = createDataverseRequest(findUserOrDie());
dataverseRequest = createDataverseRequest(findAuthenticatedUserOrDie());
} catch (WrappedResponse wr) {
List<String> 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<AuthenticatedUser> requesters = dataFile.getFileAccessRequesters();

if (requesters == null || requesters.isEmpty()) {
List<String> 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();
Expand Down Expand Up @@ -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"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class AssignRoleCommand extends AbstractCommand<RoleAssignment> {

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;
Expand All @@ -45,7 +46,7 @@ public class AssignRoleCommand extends AbstractCommand<RoleAssignment> {
*/
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;
Expand Down Expand Up @@ -76,7 +77,7 @@ public Map<String, Set<Permission>> 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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above on permissions, but if we say it applies to files, should the affected object just be the file now? (added here as github didn't seem to let me add this comment to line 48)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose this because I think that makes it appear as one of the dataset-level permissions you can assign on a dataset. The language is confusing but I think this currently is a dataset-level permission allowing you to grant the individual file-level download permissions.

I think it could be redesigned as a file-level permission to that let's you assign download permissions to that file, but I'm not sure that's needed and it seems like a lot of extra assignment points.

I may be missing something in all of this though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, to be clear - roles are what appear to assign. But yes roles will only show up if they have permissions for your type of object, or lower. So a role with only dataverse level permissions will not show up when you assign a role at the dataset level. BUT do note the "or lower" part. Since roles can cascade down, a role with only file level permissions will still show up when you assign a role at the dataset level. For example, file downloader.

Changing it now to file level won't change anything about the way you have it working, users will still assign these roles at the dataset and it will apply to granting permission for ALL files in that dataset. But it would allow us to some day add a workflow to have someone only grant access to some files.

It seems to me a small enough change - change the level on the permission and the affected object on those two commands, and you can see it will still work as designed for assigning at the dataset.

My vote would be to change this now, BUT if we decide not to change it now; let's add a comment on the permission so we know what we need to to make this change in the future sometime.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK but I'm still confused some about the specific change: I can change the object type to DataFile in the Permissions class. Is there anything else?

The Assign/Revoke role commands are reporting the owning Dataset as the object if the role is on a file already - for the DownloadPermission. Changing the ManageFilePermissions to use DataFile, would without further changes, still report it as affecting the owning Dataset. Whereas if I change the commands to report DataFile as the affected object, it would affect DownloadPermission as well. So - any change in the Commands now?

On the UI side, the menu appears/disappears if you have ManageFilePermissions on the Dataset and the canManageFilesOnDataset(Dataset) method checks that Dataset. To really use this on DataFiles, we'd eventually need a canManageFile(DataFile) method as well? (instead?) Is there any change in these areas at this point?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qqmyers let's discuss briefly today and we can decide on the change or not and move along.

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand All @@ -35,10 +34,10 @@ protected void executeImpl(CommandContext ctxt) throws CommandException {

@Override
public Map<String, Set<Permission>> 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() {
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/propertyFiles/Bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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$;
6 changes: 3 additions & 3 deletions src/main/webapp/dataset.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -384,18 +384,18 @@
<h:outputText value="#{bundle['dataset.editBtn.itemLabel.terms']}" />
</p:commandLink>
</li>
<ui:fragment rendered="#{permissionsWrapper.canManagePermissions(DatasetPage.dataset)}">
<ui:fragment rendered="#{permissionsWrapper.canManagePermissions(DatasetPage.dataset) || permissionsWrapper.canManageFilesOnDataset(DatasetPage.dataset)}">
<li class="#{settingsWrapper.publicInstall ? '' : 'dropdown-submenu pull-left'}">
<ui:fragment rendered="#{!settingsWrapper.publicInstall}">
<a tabindex="-1" href="#">#{bundle['dataset.editBtn.itemLabel.permissions']}</a>
<ul class="dropdown-menu">
<li>
<li jsf:rendered="#{permissionsWrapper.canManagePermissions(DatasetPage.dataset)}">
<h:link id="manageDatasetPermissions" styleClass="ui-commandlink ui-widget" outcome="permissions-manage">
<h:outputText value="#{bundle['dataset']}" />
<f:param name="id" value="#{DatasetPage.dataset.id}" />
</h:link>
</li>
<li>
<li jsf:rendered="#{permissionsWrapper.canManageFilesOnDataset(DatasetPage.dataset)}">
<h:link id="manageFilePermissions" styleClass="ui-commandlink ui-widget" outcome="permissions-manage-files">
<h:outputText value="#{bundle['file']}" />
<f:param name="id" value="#{DatasetPage.dataset.id}" />
Expand Down