diff --git a/doc/release-notes/11534-link-permissions.md b/doc/release-notes/11534-link-permissions.md new file mode 100644 index 00000000000..29251c1b7d9 --- /dev/null +++ b/doc/release-notes/11534-link-permissions.md @@ -0,0 +1,3 @@ +Linking or unlinking a dataset or dataverse now requires the new "Link Dataset/Dataverse" permission. +Previously, this action was covered by the "Publish Dataset/Dataverse" permission. +Linking and publishing permissions can now be granted separately, allowing for more fine-grained access control. \ No newline at end of file diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index c6d325a9651..a37819c90e1 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -118,7 +118,7 @@ Moves a dataset whose id is passed to a Dataverse collection whose alias is pass Link a Dataset ^^^^^^^^^^^^^^ -Creates a link between a dataset and a Dataverse collection (see the :ref:`dataset-linking` section of the User Guide for more information). :: +Creates a link between a dataset and a Dataverse collection (see the :ref:`dataset-linking` section of the User Guide for more information). Accessible to users with Link Dataset permission on the Dataverse collection. :: curl -H "X-Dataverse-key: $API_TOKEN" -X PUT http://$SERVER/api/datasets/$linked-dataset-id/link/$linking-dataverse-alias @@ -155,7 +155,7 @@ It returns a list in the following format (new format as of v6.4): Unlink a Dataset ^^^^^^^^^^^^^^^^ -Removes a link between a dataset and a Dataverse collection. Accessible to users with Publish Dataset permissions. :: +Removes a link between a dataset and a Dataverse collection. Accessible to users with Link Dataset permission on the Dataverse collection. :: curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/datasets/$linked-dataset-id/deleteLink/$linking-dataverse-alias diff --git a/doc/sphinx-guides/source/user/dataverse-management.rst b/doc/sphinx-guides/source/user/dataverse-management.rst index ecb1f608c12..bec155e3d32 100755 --- a/doc/sphinx-guides/source/user/dataverse-management.rst +++ b/doc/sphinx-guides/source/user/dataverse-management.rst @@ -215,7 +215,7 @@ Dataset linking allows a Dataverse collection owner to "link" their Dataverse co For example, researchers working on a collaborative study across institutions can each link their own individual institutional Dataverse collections to the one collaborative dataset, making it easier for interested parties from each institution to find the study. -In order to link a dataset, you will need your account to have the "Publish Dataset" permission on the Dataverse collection that is doing the linking. If you created the Dataverse collection then you should have this permission already, but if not then you will need to ask the admin of that Dataverse collection to assign that permission to your account. You do not need any special permissions on the dataset being linked. +In order to link a dataset, you will need your account to have the "Link Dataset" permission on the Dataverse collection that is doing the linking. If you created the Dataverse collection then you should have this permission already, but if not then you will need to ask the admin of that Dataverse collection to assign that permission to your account. You do not need any special permissions on the dataset being linked. To link a dataset to your Dataverse collection, you must navigate to that dataset and click the white "Link" button in the upper-right corner of the dataset page. This will open up a window where you can type in the name of the Dataverse collection that you would like to link the dataset to. Select your Dataverse collection and click the save button. This will establish the link, and the dataset will now appear under your Dataverse collection. diff --git a/scripts/api/data/role-curator.json b/scripts/api/data/role-curator.json index 91cb7ec43e2..86f18b2ea6a 100644 --- a/scripts/api/data/role-curator.json +++ b/scripts/api/data/role-curator.json @@ -1,13 +1,14 @@ { "alias":"curator", "name":"Curator", - "description":"For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.", + "description":"For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.", "permissions":[ "ViewUnpublishedDataset", "EditDataset", "DownloadFile", "DeleteDatasetDraft", "PublishDataset", + "LinkDataset", "ManageDatasetPermissions", "ManageFilePermissions", "AddDataverse", diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java index f89e707cc03..3879b8a3c81 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java @@ -512,7 +512,7 @@ public List filterDataversesForLinking(String query, DataverseRequest for (Dataverse res : results) { if (!remove.contains(res)) { - if (this.permissionService.requestOn(req, res).has(Permission.PublishDataset)) { + if (this.permissionService.requestOn(req, res).has(Permission.LinkDataset)) { dataverseList.add(res); } } @@ -525,7 +525,7 @@ public List filterDataversesForUnLinking(String query, DataverseReque List dataverseList = new ArrayList<>(); if (alreadyLinkeddv_ids != null && !alreadyLinkeddv_ids.isEmpty()) { alreadyLinkeddv_ids.stream().map((testDVId) -> this.find(testDVId)).forEachOrdered((dataverse) -> { - if (this.permissionService.requestOn(req, dataverse).has(Permission.PublishDataset)) { + if (this.permissionService.requestOn(req, dataverse).has(Permission.LinkDataset)) { dataverseList.add(dataverse); } }); 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 32937098118..83dc9965f6f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java @@ -48,7 +48,9 @@ public enum Permission implements java.io.Serializable { ManageDatasetPermissions(BundleUtil.getStringFromBundle("permission.managePermissionsDataset"), true, Dataset.class), ManageFilePermissions(BundleUtil.getStringFromBundle("permission.managePermissionsDataFile"), true, DataFile.class), PublishDataverse(BundleUtil.getStringFromBundle("permission.publishDataverse"), true, Dataverse.class), + LinkDataverse(BundleUtil.getStringFromBundle("permission.linkDataverse"), true, Dataverse.class), PublishDataset(BundleUtil.getStringFromBundle("permission.publishDataset"), true, Dataset.class, Dataverse.class), + LinkDataset(BundleUtil.getStringFromBundle("permission.linkDataset"), true, Dataset.class, Dataverse.class), // Delete DeleteDataverse(BundleUtil.getStringFromBundle("permission.deleteDataverse"), true, Dataverse.class), DeleteDatasetDraft(BundleUtil.getStringFromBundle("permission.deleteDataset"), true, Dataset.class); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java index 7f5672c0cd7..adc973df6a9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java @@ -23,14 +23,14 @@ * @author sarahferry */ -@RequiredPermissions( Permission.PublishDataset ) +@RequiredPermissions( Permission.LinkDataset ) public class DeleteDatasetLinkingDataverseCommand extends AbstractCommand{ private final DatasetLinkingDataverse doomed; private final Dataset editedDs; private final boolean index; public DeleteDatasetLinkingDataverseCommand(DataverseRequest aRequest, Dataset editedDs , DatasetLinkingDataverse doomed, boolean index) { - super(aRequest, editedDs); + super(aRequest, doomed.getLinkingDataverse()); this.editedDs = editedDs; this.doomed = doomed; this.index = index; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDatasetCommand.java index aef749d7e26..3cd3520f9f2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDatasetCommand.java @@ -26,7 +26,7 @@ * * @author skraffmiller */ -@RequiredPermissions(Permission.PublishDataset) +@RequiredPermissions(Permission.LinkDataset) public class LinkDatasetCommand extends AbstractCommand { private final Dataset linkedDataset; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java index 55fe96556a5..2e1aecc9a84 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java @@ -31,7 +31,7 @@ * * @author skraffmiller */ -@RequiredPermissions(Permission.PublishDataverse) +@RequiredPermissions(Permission.LinkDataverse) public class LinkDataverseCommand extends AbstractCommand { private final Dataverse linkedDataverse; @@ -47,7 +47,7 @@ public LinkDataverseCommand(DataverseRequest aRequest, Dataverse dataverse, Data public DataverseLinkingDataverse execute(CommandContext ctxt) throws CommandException { if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { throw new PermissionException("Link Dataverse can only be called by superusers.", - this, Collections.singleton(Permission.PublishDataverse), linkingDataverse); + this, Collections.singleton(Permission.LinkDataverse), linkingDataverse); } if (linkedDataverse.equals(linkingDataverse)) { throw new IllegalCommandException("Can't link a dataverse to itself", this); diff --git a/src/main/java/propertyFiles/BuiltInRoles.properties b/src/main/java/propertyFiles/BuiltInRoles.properties index 026df600a9c..50dbb1ba80f 100644 --- a/src/main/java/propertyFiles/BuiltInRoles.properties +++ b/src/main/java/propertyFiles/BuiltInRoles.properties @@ -3,7 +3,7 @@ role.admin.description=A person who has all permissions for dataverses, datasets role.contributor.name=Contributor role.contributor.description=For datasets, a person who can edit License + Terms, and then submit them for review. role.curator.name=Curator -role.curator.description=For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets. +role.curator.description=For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets. role.dscontributor.name=Dataset Creator role.dscontributor.description=A person who can add datasets within a dataverse. role.fullcontributor.name=Dataverse + Dataset Creator diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index d7fd94bf65c..d9d802b98bb 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2555,7 +2555,9 @@ permission.addDataverseDataverse=Add a dataverse within another dataverse permission.deleteDataset=Delete a dataset draft permission.deleteDataverse=Delete an unpublished dataverse permission.publishDataset=Publish a dataset +permission.linkDataset=Link a dataset to a dataverse permission.publishDataverse=Publish a dataverse +permission.linkDataverse=Link a dataverse to another dataverse permission.managePermissionsDataFile=Manage permissions for a file permission.managePermissionsDataset=Manage permissions for a dataset permission.managePermissionsDataverse=Manage permissions for a dataverse @@ -2907,7 +2909,9 @@ permission.EditDataset.label=EditDataset permission.ManageDataversePermissions.label=ManageDataversePermissions permission.ManageDatasetPermissions.label=ManageDatasetPermissions permission.PublishDataverse.label=PublishDataverse +permission.LinkDataverse.label=LinkDataverse permission.PublishDataset.label=PublishDataset +permission.LinkDataset.label=LinkDataset permission.DeleteDataverse.label=DeleteDataverse permission.DeleteDatasetDraft.label=DeleteDatasetDraft permission.ManageFilePermissions.label=ManageFilePermissions 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 7a227b02349..fa690687564 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -2792,13 +2792,19 @@ public void testDcmChecksumValidationMessages() throws IOException, InterruptedE @Test public void testCreateDeleteDatasetLink() { + // Create superuser Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); String username = UtilIT.getUsernameFromResponse(createUser); String apiToken = UtilIT.getApiTokenFromResponse(createUser); - Response superuserResponse = UtilIT.makeSuperUser(username); + // Create another user that doesn't have permission to create/delete links + Response createUser2 = UtilIT.createRandomUser(); + createUser2.prettyPrint(); + String username2 = UtilIT.getUsernameFromResponse(createUser2); + String apiToken2 = UtilIT.getApiTokenFromResponse(createUser2); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); createDataverseResponse.prettyPrint(); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); @@ -2834,28 +2840,31 @@ public void testCreateDeleteDatasetLink() { publishDatasetForLinking.prettyPrint(); publishTargetDataverse.then().assertThat() .statusCode(OK.getStatusCode()); - - // And link the dataset to this new dataverse: + + // Try to link the dataset to the new dataverse without LinkDataset permissions + createLinkingDatasetResponse = UtilIT.createDatasetLink(datasetId.longValue(), dataverseAlias, apiToken2); + createLinkingDatasetResponse.prettyPrint(); + createLinkingDatasetResponse.then().assertThat() + .body("message", equalTo("User @" + username2 + " is not permitted to perform requested action.")) + .statusCode(UNAUTHORIZED.getStatusCode()); + + // Link the dataset to the new dataverse createLinkingDatasetResponse = UtilIT.createDatasetLink(datasetId.longValue(), dataverseAlias, apiToken); createLinkingDatasetResponse.prettyPrint(); createLinkingDatasetResponse.then().assertThat() .body("data.message", equalTo("Dataset " + datasetId +" linked successfully to " + dataverseAlias)) .statusCode(200); - // Create a new user that doesn't have permission to delete the link - Response createUser2 = UtilIT.createRandomUser(); - createUser2.prettyPrint(); - String username2 = UtilIT.getUsernameFromResponse(createUser2); - String apiToken2 = UtilIT.getApiTokenFromResponse(createUser2); - // Try to delete the link without PublishDataset permissions + // Try to delete the link without LinkDataset permissions Response deleteLinkingDatasetResponse = UtilIT.deleteDatasetLink(datasetId.longValue(), dataverseAlias, apiToken2); deleteLinkingDatasetResponse.prettyPrint(); deleteLinkingDatasetResponse.then().assertThat() .body("message", equalTo("User @" + username2 + " is not permitted to perform requested action.")) .statusCode(UNAUTHORIZED.getStatusCode()); - // Add the Curator role to this user to show that they can delete the link later. (Timing issues if you try to delete right after giving permission) - Response givePermissionResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, "curator", "@" + username2, apiToken); + // Give the user curator rights for the target dataverse to show that they can add and delete the link later + // (Timing issues if you try to add or delete right after giving permission) + Response givePermissionResponse = UtilIT.grantRoleOnDataverse(dataverseAlias, "curator", "@" + username2, apiToken); givePermissionResponse.prettyPrint(); givePermissionResponse.then().assertThat() .statusCode(200); @@ -2868,17 +2877,16 @@ public void testCreateDeleteDatasetLink() { .body("data.message", equalTo("Link from Dataset " + datasetId + " to linked Dataverse " + dataverseAlias + " deleted")) .statusCode(200); - // And re-link the dataset to this new dataverse: - createLinkingDatasetResponse = UtilIT.createDatasetLink(datasetId.longValue(), dataverseAlias, apiToken); + // And now test linking the dataset as user2 with new role as curator (link permissions): + createLinkingDatasetResponse = UtilIT.createDatasetLink(datasetId.longValue(), dataverseAlias, apiToken2); createLinkingDatasetResponse.prettyPrint(); createLinkingDatasetResponse.then().assertThat() .body("data.message", equalTo("Dataset " + datasetId +" linked successfully to " + dataverseAlias)) .statusCode(200); - // And now test deleting it as user2 with new role as curator (Publish permissions): + // And now test deleting it as user2 with new role as curator (link permissions): deleteLinkingDatasetResponse = UtilIT.deleteDatasetLink(datasetId.longValue(), dataverseAlias, apiToken2); deleteLinkingDatasetResponse.prettyPrint(); - deleteLinkingDatasetResponse.then().assertThat() .body("data.message", equalTo("Link from Dataset " + datasetId + " to linked Dataverse " + dataverseAlias + " deleted")) .statusCode(200);