diff --git a/doc/release-notes/10588-power-user-role.md b/doc/release-notes/10588-power-user-role.md new file mode 100644 index 00000000000..3ecc405d3ca --- /dev/null +++ b/doc/release-notes/10588-power-user-role.md @@ -0,0 +1,10 @@ +### Delegated Administrative Permissions ("Power User") + +A new "Power User" role and "ScopedPowerUser" permission have been introduced to support delegated administrative tasks. This allows instance administrators to grant users administrative privileges over specific collections or datasets without granting them global superuser status. + +Users with the Power User role (or any role containing the ScopedPowerUser permission) assigned on a specific Dataverse object can perform various tasks previously restricted to global superusers, including: +- Moving Dataverse collections and datasets. +- Managing PIDs and registration metadata. +- Configuring storage quotas and drivers. +- Accessing administrative dashboard tools for their scoped objects. +- Bypassing certain upload limits and managing curation labels. diff --git a/doc/sphinx-guides/source/admin/collectionquotas.rst b/doc/sphinx-guides/source/admin/collectionquotas.rst index 2ce3132e2ba..91299ba7ec5 100644 --- a/doc/sphinx-guides/source/admin/collectionquotas.rst +++ b/doc/sphinx-guides/source/admin/collectionquotas.rst @@ -4,7 +4,7 @@ Storage Quotas for Collections Please note that this is a new and still experimental feature (as of Dataverse v6.1 release). -Instance admins can now define storage quota limits for specific collections. These limits can be set, changed and/or deleted via the provided APIs (please see the :ref:`collection-storage-quotas` section of the :doc:`/api/native-api` guide). The Read version of the API is available to the individual collection admins (i.e., a collection owner can check on the quota configured for their collection), but only superusers can set, change or disable storage quotas. +Instance admins can now define storage quota limits for specific collections. These limits can be set, changed and/or deleted via the provided APIs (please see the :ref:`collection-storage-quotas` section of the :doc:`/api/native-api` guide). The Read version of the API is available to the individual collection admins (i.e., a collection owner can check on the quota configured for their collection), but only superusers or power users can set, change or disable storage quotas. Storage quotas are *inherited* by subcollections. In other words, when storage use limit is set for a specific collection, it applies to all the datasets immediately under it and in its sub-collections, unless different quotas are defined there and so on. Each file added to any dataset in that hierarchy counts for the purposes of the quota limit defined for the top collection. A storage quota defined on a child sub-collection overrides whatever quota that may be defined on the parent, or inherited from an ancestor. diff --git a/doc/sphinx-guides/source/admin/dashboard.rst b/doc/sphinx-guides/source/admin/dashboard.rst index 3f77729d0ab..d5c22d5d2fe 100644 --- a/doc/sphinx-guides/source/admin/dashboard.rst +++ b/doc/sphinx-guides/source/admin/dashboard.rst @@ -1,7 +1,7 @@ Dashboard ========= -The Dataverse Software offers a dashboard of administrative tools for superusers only. If you are a logged-in superuser, you can access it by clicking your username in the navbar, and then clicking "Dashboard" from the dropdown. You can verify that you are a superuser by checking the color of your username in the navbar. If it's red, you have the right permissions to use the Dashboard. Superusers can give other users the superuser status via :doc:`user-administration`. +The Dataverse Software offers a dashboard of administrative tools for superusers and power users. While superusers have access to all tools, power users (those with the "Power User" role assigned on a specific Dataverse collection or dataset) only see tools that are relevant to their scoped objects, such as "Move Data". If you are a logged-in superuser or power user, you can access the dashboard by clicking your username in the navbar, and then clicking "Dashboard" from the dropdown. You can verify that you are a superuser by checking the color of your username in the navbar. If it's red, you have the right permissions to use all parts of the Dashboard. Superusers can give other users the superuser status via :doc:`user-administration`. .. contents:: Contents: :local: diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index dc4dfabfedc..d0cc1900044 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -15,7 +15,7 @@ Dataverse collections have to be empty to delete them. Navigate to the Dataverse Move a Dataverse Collection ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Moves a Dataverse collection whose id is passed to an existing Dataverse collection whose id is passed. The Dataverse collection alias also may be used instead of the id. If the moved Dataverse collection has a guestbook, template, metadata block, link, or featured Dataverse collection that is not compatible with the destination Dataverse collection, you will be informed and given the option to force the move and remove the association. Only accessible to superusers. :: +Moves a Dataverse collection whose id is passed to an existing Dataverse collection whose id is passed. The Dataverse collection alias also may be used instead of the id. If the moved Dataverse collection has a guestbook, template, metadata block, link, or featured Dataverse collection that is not compatible with the destination Dataverse collection, you will be informed and given the option to force the move and remove the association. Only accessible to superusers or users with the ScopedPowerUser permission on the Dataverse collection. :: curl -H "X-Dataverse-key: $API_TOKEN" -X POST http://$SERVER/api/dataverses/$id/move/$destination-id @@ -36,14 +36,14 @@ Removes a link between a Dataverse collection and another Dataverse collection. List Dataverse Collection Links ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Provides information about whether a certain Dataverse collection ($dataverse-alias) is linked to or links to another collection. Only accessible to superusers. :: +Provides information about whether a certain Dataverse collection ($dataverse-alias) is linked to or links to another collection. Only accessible to superusers or power users. :: curl -H "X-Dataverse-key:$API_TOKEN" http://$SERVER/api/dataverses/$dataverse-alias/links Add Dataverse Collection RoleAssignments to Dataverse Subcollections ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Recursively assigns the users and groups having a role(s),that are in the set configured to be inheritable via the :InheritParentRoleAssignments setting, on a specified Dataverse collections to have the same role assignments on all of the Dataverse collections that have been created within it. The response indicates success or failure and lists the individuals/groups and Dataverse collections involved in the update. Only accessible to superusers. :: +Recursively assigns the users and groups having a role(s),that are in the set configured to be inheritable via the :InheritParentRoleAssignments setting, on a specified Dataverse collections to have the same role assignments on all of the Dataverse collections that have been created within it. The response indicates success or failure and lists the individuals/groups and Dataverse collections involved in the update. Only accessible to superusers or power users. :: curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/$dataverse-alias/addRoleAssignmentsToChildren @@ -85,7 +85,7 @@ Datasets within a given Dataverse collection can be annotated with a Curation La The label is applied to a draft Dataset version via the user interface or API and the available label sets are defined by :ref:`:AllowedCurationLabels <:AllowedCurationLabels>`. Internally, the labels have no effect, and at publication, any existing label will be removed. A reporting API call allows admins to get a list of datasets and their curation statuses. -The label set used for a collection can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers. +The label set used for a collection can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers or power users. The curationLabelSet to use within a given collection can be set by specifying its name using:: @@ -178,7 +178,7 @@ Datasets Move a Dataset ^^^^^^^^^^^^^^ -Superusers can move datasets using the dashboard. See also :doc:`dashboard`. +Superusers and power users can move datasets using the dashboard. See also :doc:`dashboard`. Moves a dataset whose id is passed to a Dataverse collection whose alias is passed. If the moved dataset has a guestbook or a Dataverse collection link that is not compatible with the destination Dataverse collection, you will be informed and given the option to force the move (with ``forceMove=true`` as a query parameter) and remove the guestbook or link (or both). Only accessible to users with permission to publish the dataset in the original and destination Dataverse collection. Note: any roles granted to users on the dataset will continue to be in effect after the dataset has been moved. :: @@ -266,7 +266,7 @@ The application will attempt to sleep for 1 second between registration attempts Mint a New DOI for a Dataset with a Handle ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Mints a new identifier for a dataset previously registered with a handle. Only accessible to superusers. :: +Mints a new identifier for a dataset previously registered with a handle. Only accessible to superusers or power users. :: curl -H "X-Dataverse-key: $API_TOKEN" -X POST http://$SERVER/api/admin/$dataset-id/reregisterHDLToPID @@ -276,7 +276,7 @@ Update Target URL for a Published Dataset at the PID provider ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Forces update to the target URL provided to the PID provider of a published dataset and assures the PID is findable. -Only accessible to superusers. :: +Only accessible to superusers or power users. :: curl -H "X-Dataverse-key: $API_TOKEN" -X POST http://$SERVER/api/datasets/$dataset-id/modifyRegistration @@ -293,7 +293,7 @@ Update Metadata for a Published Dataset at the PID provider Checks to see that the PID metadata for a published dataset (and any released files in it using file PIDs) is up-to-date at the provider and updates the metadata if necessary. -Only accessible to superusers. :: +Only accessible to superusers or power users. :: curl -H "X-Dataverse-key: $API_TOKEN" -X POST http://$SERVER/api/datasets/$dataset-id/modifyRegistrationMetadata @@ -316,7 +316,7 @@ See :ref:`pids-api` in the API Guide for details. Make Metadata Updates Without Changing Dataset Version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -As a superuser, click "Update Current Version" when publishing. (This option is only available when a 'Minor' update would be allowed.) +As a superuser or power user, click "Update Current Version" when publishing. (This option is only available when a 'Minor' update would be allowed.) Diagnose Constraint Violations Issues in Datasets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -326,7 +326,7 @@ To identify invalid data values in specific datasets (if, for example, an attemp Configure a Dataset to Store All New Files in a Specific File Store ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Configure an individual dataset to use a specific file store (this API can only be used by a superuser) :: +Configure an individual dataset to use a specific file store (this API can only be used by a superuser or power user) :: curl -H "X-Dataverse-key: $API_TOKEN" -X PUT -d $storageDriverLabel http://$SERVER/api/datasets/$dataset-id/storageDriver @@ -336,7 +336,7 @@ The effective store can be seen using:: The output of the API will include the id, label, type (for example, "file" or "s3") as well as the support for direct download and upload. -To remove an assigned store, and allow the dataset to inherit the store from it's parent collection, use the following (only a superuser can do this) :: +To remove an assigned store, and allow the dataset to inherit the store from it's parent collection, use the following (only a superuser or power user can do this) :: curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/datasets/$dataset-id/storageDriver @@ -351,7 +351,7 @@ A dataset can be annotated with a Curation Label to indicate the status of the d The label is applied to a draft Dataset version via the user interface or API and the available label sets are defined by :ref:`:AllowedCurationLabels <:AllowedCurationLabels>`. Internally, the labels have no effect, and at publication, any existing label will be removed. A reporting API call allows admins to get a list of datasets and their curation statuses. -The label set used for a dataset can be specified via the API as shown below. Only accessible to superusers. +The label set used for a dataset can be specified via the API as shown below. Only accessible to superusers or power users. The curationLabelSet to use within a given dataset can be set by specifying its name using:: @@ -363,7 +363,7 @@ The name of the current curationLabelSet can be seen using:: curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/datasets/$dataset-id/curationLabelSet -and can be reset to the default (inherited from the parent collection) with (only a superuser can do this) :: +and can be reset to the default (inherited from the parent collection) with (only a superuser or power user can do this) :: curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/datasets/$dataset-id/curationLabelSet diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 84f790b7730..ea27c1ec353 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -514,7 +514,7 @@ controlledvocabulary.language.marathi_(marathi)=Marathi (Mar\u0101\u1E6Dh\u012B) Enabling a Metadata Block ~~~~~~~~~~~~~~~~~~~~~~~~~ -Running a curl command like "load" example above should make the new custom metadata block available within the system but in order to start using the fields you must either enable it from the UI (see :ref:`general-information` section of Dataverse Collection Management in the User Guide) or by running a curl command like the one below using a superuser API token. In the example below we are enabling the "journal" and "geospatial" metadata blocks for the root Dataverse collection: +Running a curl command like "load" example above should make the new custom metadata block available within the system but in order to start using the fields you must either enable it from the UI (see :ref:`general-information` section of Dataverse Collection Management in the User Guide) or by running a curl command like the one below using a superuser or power user API token. In the example below we are enabling the "journal" and "geospatial" metadata blocks for the root Dataverse collection: ``curl -H "X-Dataverse-key:$API_TOKEN" -X POST -H "Content-type:application/json" -d "[\"journal\",\"geospatial\"]" http://localhost:8080/api/dataverses/:root/metadatablocks`` diff --git a/doc/sphinx-guides/source/api/curation-labels.rst b/doc/sphinx-guides/source/api/curation-labels.rst index 50ac05b670f..ca5db4e3227 100644 --- a/doc/sphinx-guides/source/api/curation-labels.rst +++ b/doc/sphinx-guides/source/api/curation-labels.rst @@ -2,7 +2,7 @@ Dataset Curation Status API =========================== When the :ref:`:AllowedCurationLabels <:AllowedCurationLabels>` setting has been used to define Curation Labels, this API can be used to set these labels on draft datasets. -Superusers can define which set of labels are allowed for a given datasets in a collection/an individual dataset using the api described in the :doc:`/admin/dataverses-datasets` section. +Superusers and power users can define which set of labels are allowed for a given datasets in a collection/an individual dataset using the api described in the :doc:`/admin/dataverses-datasets` section. The API here can be used by curators/those who have permission to publish the dataset to get/set/change/delete the label currently assigned to a draft dataset. If the :ref:`dataverse.ui.show-curation-status-to-all` flag is enabled, users who can see the draft dataset version can use the get API call. diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 1806772afbd..89b6b990276 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -510,7 +510,7 @@ The fully expanded example above (without environment variables) looks like this For ``roles.json`` see :ref:`json-representation-of-a-role` -.. note:: Only a Dataverse installation account with superuser permissions is allowed to create roles in a Dataverse Collection. +.. note:: Only a Dataverse installation account with superuser permissions or a power user is allowed to create roles in a Dataverse Collection. .. _list-role-assignments-on-a-dataverse-api: @@ -948,7 +948,7 @@ List Locally FAIR Role Assignees for a Dataverse Collection Lists the Locally FAIR role assignee identifiers configured for a Dataverse collection identified by ``id``. For more about the concept, see :ref:`locally-fair` in the User Guide. -This API is superuser-only. +This API is only accessible to superusers or power users. .. code-block:: bash @@ -980,7 +980,7 @@ Set Locally FAIR Role Assignees for a Dataverse Collection Replaces the full set locally FAIR role assignee identifiers for a Dataverse collection identified by ``id``. -This API is superuser-only. +This API is only accessible to superusers or power users. .. code-block:: bash @@ -1012,7 +1012,7 @@ Add a Locally FAIR Role Assignee to a Dataverse Collection Adds a single locally FAIR role assignee identifier to a Dataverse collection identified by ``id``. -This API is superuser-only. +This API is only accessible to superusers or power users. .. code-block:: bash @@ -1038,7 +1038,7 @@ Delete a Locally FAIR Role Assignee from a Dataverse Collection Removes a single locally FAIR role assignee identifier from a Dataverse collection identified by ``id``. -This API is superuser-only. +This API is only accessible to superusers or power users. .. code-block:: bash @@ -1180,7 +1180,7 @@ See also :ref:`dataset-types`. Import a Dataset into a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: This action requires a Dataverse installation account with super-user permissions. +.. note:: This action requires a Dataverse installation account with superuser or power user permissions. To import a dataset with an existing persistent identifier (PID), the dataset's metadata should be prepared in Dataverse installation's native JSON format. The PID is provided as a parameter at the URL. The following line imports a dataset with the PID ``PERSISTENT_IDENTIFIER`` to the Dataverse installation, and then releases it: @@ -1233,7 +1233,7 @@ See also :ref:`dataset-types`. Import a Dataset into a Dataverse Installation with a DDI file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: This action requires a Dataverse installation account with super-user permissions. +.. note:: This action requires a Dataverse installation account with superuser or power user permissions. To import a dataset with an existing persistent identifier (PID), you have to provide the PID as a parameter at the URL. The following line imports a dataset with the PID ``PERSISTENT_IDENTIFIER`` to the Dataverse installation, and then releases it: @@ -1482,9 +1482,9 @@ The following attributes are supported: * ``name`` Name * ``description`` Description * ``affiliation`` Affiliation -* ``filePIDsEnabled`` ("true" or "false") Restricted to use by superusers and only when the :ref:`:AllowEnablingFilePIDsPerCollection <:AllowEnablingFilePIDsPerCollection>` setting is true. Enables or disables registration of file-level PIDs in datasets within the collection (overriding the instance-wide setting). -* ``requireFilesToPublishDataset`` ("true" or "false") Restricted to use by superusers. Defines if Dataset version needs files in order to be published or submitted for review. If not set the determination will be made through inheritance by checking the owners of this collection. Publishing by a superusers will not be blocked. -* ``allowedDatasetTypes`` Restricted to use by superusers. By default "dataset" is implied. Pass a comma-separated list of dataset types (e.g. "dataset,software"). You cannot unset this attribute so if you want to delete a dataset type, set ``allowedDatasetTypes`` to a dataset type you won't be deleting. See also :ref:`dataset-types`. +* ``filePIDsEnabled`` ("true" or "false") Restricted to use by superusers or power users and only when the :ref:`:AllowEnablingFilePIDsPerCollection <:AllowEnablingFilePIDsPerCollection>` setting is true. Enables or disables registration of file-level PIDs in datasets within the collection (overriding the instance-wide setting). +* ``requireFilesToPublishDataset`` ("true" or "false") Restricted to use by superusers or power users. Defines if Dataset version needs files in order to be published or submitted for review. If not set the determination will be made through inheritance by checking the owners of this collection. Publishing by a superuser or power user will not be blocked. +* ``allowedDatasetTypes`` Restricted to use by superusers or power users. By default "dataset" is implied. Pass a comma-separated list of dataset types (e.g. "dataset,software"). You cannot unset this attribute so if you want to delete a dataset type, set ``allowedDatasetTypes`` to a dataset type you won't be deleting. See also :ref:`dataset-types`. See also :ref:`update-dataverse-api`. @@ -1565,7 +1565,7 @@ To set or change the storage allocation quota for a collection: curl -X PUT -H "X-Dataverse-key:$API_TOKEN" -d $SIZE_IN_BYTES "$SERVER_URL/api/dataverses/$ID/storage/quota" -This API is superuser-only. +This API is only accessible to superusers or power users. To delete a storage quota configured for a collection: @@ -1574,7 +1574,7 @@ To delete a storage quota configured for a collection: curl -X DELETE -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/storage/quota" -This API is superuser-only. +This API is only accessible to superusers or power users. Storage Quotas on Individual Datasets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1598,7 +1598,7 @@ To set or change the storage allocation quota for a dataset: curl -X PUT -H "X-Dataverse-key:$API_TOKEN" -d $SIZE_IN_BYTES "$SERVER_URL/api/datasets/$ID/storage/quota" -This API is superuser-only. +This API is only accessible to superusers or power users. To delete a storage quota configured for a dataset: @@ -1607,7 +1607,7 @@ To delete a storage quota configured for a dataset: curl -X DELETE -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/datasets/$ID/storage/quota" -This API is superuser-only. +This API is only accessible to superusers or power users. The following convenience API shows the dynamic values of the *remaining* storage size and/or file number quotas on the dataset, if present. For example: diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 360a96e8eb5..93575820347 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -330,6 +330,8 @@ Root Dataverse Collection Permissions The user who creates a Dataverse collection is given the "Admin" role on that Dataverse collection. The root Dataverse collection is created automatically for you by the installer and the "Admin" is the superuser account ("dataverseAdmin") we used in the :doc:`installation-main` section to confirm that we can log in. These next steps of configuring the root Dataverse collection require the "Admin" role on the root Dataverse collection, but not the much more powerful superuser attribute. In short, users with the "Admin" role are subject to the permission system. A superuser, on the other hand, completely bypasses the permission system. You can give non-superusers the "Admin" role on the root Dataverse collection if you'd like them to configure the root Dataverse collection. +Additionally, instance administrators can grant the "Power User" role to specific users on a per-collection or per-dataset basis. This role allows delegated administrators to perform many tasks that were previously restricted to global superusers, such as moving objects, managing PIDs, and configuring storage drivers, without granting them full superuser status. + In order for non-superusers to start creating Dataverse collections or datasets, you need click "Edit" then "Permissions" and make choices about which users can add Dataverse collections or datasets within the root Dataverse collection. (There is an API endpoint for this operation as well.) Again, the user who creates a Dataverse collection will be granted the "Admin" role on that Dataverse collection. Non-superusers who are not "Admin" on the root Dataverse collection will not be able to do anything useful until the root Dataverse collection has been published. As the person installing the Dataverse Software, you may or may not be a local metadata expert. You may want to have others sign up for accounts and grant them the "Admin" role at the root Dataverse collection to configure metadata fields, templates, browse/search facets, guestbooks, etc. For more on these topics, consult the :doc:`/user/dataverse-management` section of the User Guide. diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index 79037cd600f..ec9d830b039 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -574,6 +574,8 @@ When you access a dataset's permissions page, you will see two sections: **Users/Groups:** Here you can assign roles to specific users or groups, determining which actions they are permitted to take on your dataset. You can also reference a list of all users who have roles assigned to them for your dataset and remove their roles if you please. Some of the users listed may have roles assigned at the Dataverse collection level, in which case those roles can only be removed from the Dataverse collection permissions page. +A special "Power User" role is also available at the dataset level, which grants a user administrative privileges over the dataset. Users with this role can perform tasks that are typically restricted to global superusers, such as moving the dataset or managing its PIDs, but only for the specific dataset where they have been assigned this role. For more information, see the :doc:`/admin/dashboard` section of the Admin Guide. + **Roles:** Here you can reference a full list of roles that can be assigned to users of your dataset. Each role lists the permissions that it offers. File-Level diff --git a/doc/sphinx-guides/source/user/dataverse-management.rst b/doc/sphinx-guides/source/user/dataverse-management.rst index 7cded79f9bf..505807265a1 100755 --- a/doc/sphinx-guides/source/user/dataverse-management.rst +++ b/doc/sphinx-guides/source/user/dataverse-management.rst @@ -149,6 +149,8 @@ Assigning Roles to Users and Groups Under the Users/Groups tab, you can add, edit, or remove the roles granted to users and groups on your Dataverse collection. A role is a set of permissions granted to a user or group when they're using your Dataverse collection. For example, giving your research assistant the "Contributor" role would give them the following self-explanatory permissions on your Dataverse collection and all datasets within your Dataverse collection: "ViewUnpublishedDataset", "DownloadFile", "EditDataset", and "DeleteDatasetDraft". They would, however, lack the "PublishDataset" permission, and thus would be unable to publish datasets on your Dataverse collection. If you wanted to give them that permission, you would give them a role with that permission, like the Curator role. Users and groups can hold multiple roles at the same time if needed. Roles can be removed at any time. All roles and their associated permissions are listed under the "Roles" tab of the same page. +A special "Power User" role is also available, which grants a user administrative privileges over a specific Dataverse collection or dataset. Users with this role can perform tasks that are typically restricted to global superusers, such as moving collections, managing storage quotas, and accessing specific administrative tools in the Dashboard, but only for the objects where they have been assigned this role. For more information, see the :doc:`/admin/dashboard` section of the Admin Guide. + |image5| Note that the Dataset Creator role and Contributor role are sometimes confused. The Dataset Creator role is assigned at the Dataverse collection level and allows a user to create new datasets in that Dataverse collection. The Contributor role can be assigned at the dataset level, granting a user the ability to edit *that specific* dataset. Alternatively, the Contributor role can be assigned at the Dataverse collection level, granting the user the ability to edit *all* datasets in that Dataverse collection. diff --git a/src/main/java/edu/harvard/iq/dataverse/DashboardPage.java b/src/main/java/edu/harvard/iq/dataverse/DashboardPage.java index c37c3f52bc7..32b853f03e8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DashboardPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DashboardPage.java @@ -15,8 +15,6 @@ import java.util.List; import java.util.logging.Logger; import jakarta.ejb.EJB; -import jakarta.faces.application.FacesMessage; -import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -42,6 +40,8 @@ public class DashboardPage implements java.io.Serializable { DataverseSession session; @Inject NavigationWrapper navigationWrapper; + @Inject + PermissionsWrapper permissionsWrapper; /* in breadcrumbs the dashboard page always appears as if it belongs to the @@ -57,7 +57,7 @@ root dataverse ("Root Dataverse -> Dashboard") - because it is for the public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); - } else if (!isSuperUser()) { + } else if (!(isSuperUser() || permissionsWrapper.isPowerUserOnSomeDvObject())) { return navigationWrapper.notAuthorized(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index ff047bec4b0..8240fb5c4f0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -105,7 +105,6 @@ import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; -import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.persistence.OptimisticLockException; @@ -1512,16 +1511,13 @@ public boolean isSessionUserAuthenticated() { * For use in the Dataset page * @return */ - public boolean isSuperUser(){ + public boolean isPowerUserOnDataset(){ if (!this.isSessionUserAuthenticated()){ return false; } - if (this.session.getUser().isSuperuser()){ - return true; - } - return false; + return permissionService.isPowerUserOn((AuthenticatedUser) session.getUser(), dataset); } /** * Check Dataset related permissions @@ -4167,7 +4163,7 @@ public String save() { // have been created in the dataset. dataset = datasetService.find(dataset.getId()); - boolean ignoreUploadFileLimits = this.session.getUser() != null ? this.session.getUser().isSuperuser() : false; + boolean ignoreUploadFileLimits = isPowerUserOnDataset(); List filesAdded = ingestService.saveAndAddFilesToDataset(dataset.getOrCreateEditVersion(), newFiles, null, true, ignoreUploadFileLimits); if (filesAdded.size() < nNewFiles) { // Not all files were saved @@ -6636,7 +6632,7 @@ public String saveEmbargo() { } for (FileMetadata fmd : workingVersion.getFileMetadatas()) { for (FileMetadata fm : embargoFMs) { - if (fm.getDataFile().equals(fmd.getDataFile()) && (isSuperUser()||!fmd.getDataFile().isReleased())) { + if (fm.getDataFile().equals(fmd.getDataFile()) && (isPowerUserOnDataset()||!fmd.getDataFile().isReleased())) { Embargo emb = fmd.getDataFile().getEmbargo(); if (emb != null) { logger.fine("Before: " + emb.getDataFiles().size()); @@ -6831,7 +6827,7 @@ public String saveRetention() { } for (FileMetadata fmd : workingVersion.getFileMetadatas()) { for (FileMetadata fm : retentionFMs) { - if (fm.getDataFile().equals(fmd.getDataFile()) && (isSuperUser()||!fmd.getDataFile().isReleased())) { + if (fm.getDataFile().equals(fmd.getDataFile()) && (isPowerUserOnDataset()||!fmd.getDataFile().isReleased())) { Retention ret = fmd.getDataFile().getRetention(); if (ret != null) { logger.fine("Before: " + ret.getDataFiles().size()); diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 76ef1002f7e..1a3ed0bc43b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -1390,7 +1390,7 @@ private void saveInputLevels(List listDFTIL, Datas )); } } - + /* Get/set methods to keep the local locallyFARIRoleAssigneesList in sync with the Dataverse's locallyFAIRRoleAssigneeIdentifiers set. */ public List getLocallyFAIRRoleAssigneesList() { diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java index f16726a8fbe..06d28ee215e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRoleServiceBean.java @@ -135,6 +135,12 @@ public List findAll() { return em.createNamedQuery("DataverseRole.listAll", DataverseRole.class).getResultList(); } + public List findAllWithPermission(Permission permission) { + return findAll().stream() + .filter(r -> r.permissions().contains(permission)) + .collect(Collectors.toList()); + } + public void delete(Long id) { em.createNamedQuery("DataverseRole.deleteById", DataverseRole.class) .setParameter("id", id) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 56831834c31..30cae7f002f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -44,7 +44,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -66,11 +65,9 @@ import edu.harvard.iq.dataverse.util.file.CreateDataFileResult; import org.primefaces.event.FileUploadEvent; import org.primefaces.model.file.UploadedFile; -import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonArray; -import jakarta.json.JsonReader; import org.apache.commons.io.IOUtils; import java.util.Arrays; import java.util.Collection; @@ -560,7 +557,7 @@ public boolean isQuotaExceeded() { return systemConfig.isStorageQuotasEnforced() && uploadSessionQuota != null && uploadSessionQuota.getRemainingQuotaInBytes() == 0; } public boolean isFileUploadCountExceeded() { - boolean ignoreLimit = this.session.getUser().isSuperuser(); + boolean ignoreLimit = permissionService.isPowerUserOn((AuthenticatedUser) session.getUser(), dataset); return !ignoreLimit && !isFileReplaceOperation() && fileUploadsAvailable != null && fileUploadsAvailable == 0; } @@ -1124,7 +1121,7 @@ public String save() { } } } - boolean ignoreUploadFileLimits = this.session.getUser() != null ? this.session.getUser().isSuperuser() : false; + boolean ignoreUploadFileLimits = (this.session.getUser() != null && this.session.getUser().isAuthenticated()) ? permissionService.isPowerUserOn((AuthenticatedUser) session.getUser(), dataset) : false; // Try to save the NEW files permanently: List filesAdded = ingestService.saveAndAddFilesToDataset(workingVersion, newFiles, null, true, ignoreUploadFileLimits); if (filesAdded.size() < nNewFiles) { diff --git a/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java b/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java index 1effd137e0e..c955ce48116 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java @@ -111,7 +111,7 @@ public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); } else if (!isSuperUser()) { - return navigationWrapper.notAuthorized(); + return navigationWrapper.notAuthorized(); } if (dataverseId != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index bd91363d2bb..230be01c7c8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -334,8 +334,8 @@ public List whichChildrenHasPermissionsFor(DataverseRequest req, DvObj User user = req.getUser(); // quick cases - if (user.isSuperuser()) { - return children; // it's good to be king + if (user instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) user, dvo)) { + return children; // it's good to be king/power user } else if (!user.isAuthenticated()) { if ( required.stream().anyMatch(PERMISSIONS_FOR_AUTHENTICATED_USERS_ONLY::contains) ){ @@ -368,7 +368,7 @@ public List whichChildrenHasPermissionsFor(DataverseRequest req, DvObj childrenAssignments.forEach( assignment -> { DvObject definitionPoint = assignment.getDefinitionPoint(); if (!roleMap.containsKey(definitionPoint)){ - roleMap.put(definitionPoint, assignment.getRole().permissions()); + roleMap.put(definitionPoint, new HashSet<>(assignment.getRole().permissions())); } else { roleMap.get(definitionPoint).addAll(assignment.getRole().permissions()); } @@ -378,7 +378,8 @@ public List whichChildrenHasPermissionsFor(DataverseRequest req, DvObj return children.stream().filter( child -> ((includeReleased && child.isReleased()) || ((roleMap.containsKey(child)) && - (roleMap.get(child).containsAll(required.stream().filter(perm -> perm.appliesTo(child.getClass())).collect(Collectors.toSet()))))) + (roleMap.get(child).contains(Permission.ScopedPowerUser) || + roleMap.get(child).containsAll(required.stream().filter(perm -> perm.appliesTo(child.getClass())).collect(Collectors.toSet()))))) ).collect( toList() ); } @@ -398,7 +399,7 @@ public List whichChildrenHasPermissionsForOrReleased(DataverseRequest public boolean hasPermissionsFor(DataverseRequest req, DvObject dvo, Set required) { User user = req.getUser(); - if (user.isSuperuser()) { + if (user instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) user, dvo)) { return true; } else if (!user.isAuthenticated()) { Set requiredCopy = EnumSet.copyOf(required); @@ -414,11 +415,12 @@ public boolean hasPermissionsFor(DataverseRequest req, DvObject dvo, Set required) { - if (ra instanceof User) { - User user = (User) ra; - if (user.isSuperuser()) { + if (ra instanceof AuthenticatedUser au) { + if (isPowerUserOn(au, dvo)) { return true; - } else if (!user.isAuthenticated()) { + } + } else if (ra instanceof User user) { + if (!user.isAuthenticated()) { Set requiredCopy = EnumSet.copyOf(required); requiredCopy.retainAll(PERMISSIONS_FOR_AUTHENTICATED_USERS_ONLY); if (!requiredCopy.isEmpty()) { @@ -452,7 +454,7 @@ private boolean hasGroupPermissionsFor(Set ras, DvObject dvo, Set< * @return Permissions of {@code req.getUser()} over {@code dvo}. */ public Set permissionsFor(DataverseRequest req, DvObject dvo) { - if (req.getUser().isSuperuser()) { + if (req.getUser() instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) req.getUser(), dvo)) { return EnumSet.allOf(Permission.class); } @@ -479,7 +481,7 @@ public Set permissionsFor(DataverseRequest req, DvObject dvo) { * @return the set of permissions {@code ra} has over {@code dvo}. */ public Set permissionsFor(RoleAssignee ra, DvObject dvo) { - if (ra instanceof AuthenticatedUser && ((AuthenticatedUser) ra).isSuperuser()) { + if (ra instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) ra, dvo)) { return EnumSet.allOf(Permission.class); } @@ -494,6 +496,54 @@ public Set permissionsFor(RoleAssignee ra, DvObject dvo) { } return permissions; } + + public boolean isPowerUserOn(AuthenticatedUser user, DvObject dvo) { + if (user == null) { + return false; + } + if (user.isSuperuser()) { + return true; + } + if (dvo == null) { + return false; + } + Set ras = new HashSet<>(groupService.groupsFor(user, dvo)); + ras.add(user); + return hasGroupPermissionsFor(ras, dvo, EnumSet.of(Permission.ScopedPowerUser)); + } + + public boolean isPowerUserOnSomeDvObject(AuthenticatedUser user) { + if (user == null) { + return false; + } + if (user.isSuperuser()) { + return true; + } + + // Get all groups the user belongs to (persistent ones) + Set groups = groupService.groupsFor(user); + Set identifiers = groups.stream() + .map(edu.harvard.iq.dataverse.authorization.groups.Group::getIdentifier) + .collect(Collectors.toSet()); + identifiers.add(user.getIdentifier()); + + // Get all roles that have ScopedPowerUser permission + List powerRoles = roleService.findAllWithPermission(Permission.ScopedPowerUser); + if (powerRoles.isEmpty()) { + return false; + } + List roleIds = powerRoles.stream() + .map(DataverseRole::getId) + .collect(Collectors.toList()); + + // Check if any role assignment exists for these identifiers and roles + Long count = em.createQuery("SELECT COUNT(ra) FROM RoleAssignment ra WHERE ra.assigneeIdentifier IN :identifiers AND ra.role.id IN :roleIds", Long.class) + .setParameter("identifiers", identifiers) + .setParameter("roleIds", roleIds) + .getSingleResult(); + + return count > 0; + } private void addGroupPermissionsFor(Set ras, DvObject dvo, Set permissions) { for (RoleAssignment asmnt : assignmentsFor(ras, dvo)) { @@ -1083,11 +1133,11 @@ public boolean hasLocallyFAIRAccess(DataverseRequest req, DvObject dvObject) { // Check if user is in the locally FAIR assignee list Set userAndGroups = new HashSet<>(groupService.groupsFor(req)); User user = req.getUser(); - if (user.isAuthenticated()) { - if(user.isSuperuser()) { + if (user instanceof AuthenticatedUser authUser) { + if (isPowerUserOn(authUser, dvObject)) { return true; } - userAndGroups.add(user); + userAndGroups.add(authUser); } for (RoleAssignee ra : userAndGroups) { diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 0af3816035a..f03dd91f8f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -7,15 +7,14 @@ import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.impl.builtin.AuthenticatedUsers; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.*; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.faces.view.ViewScoped; @@ -37,6 +36,9 @@ public class PermissionsWrapper implements java.io.Serializable { @EJB DatasetVersionServiceBean datasetVersionService; + @EJB + DataverseServiceBean dataverseService; + @Inject DataverseSession session; @@ -264,7 +266,7 @@ public boolean canIssuePublishDatasetCommand(DvObject dvo){ if (dvo == null || u == null || u instanceof GuestUser || !(dvo instanceof Dataset)) { return false; // guests can not publish } - if (u.isSuperuser()) { + if (u instanceof AuthenticatedUser && permissionService.isPowerUserOn((AuthenticatedUser) u, dvo)) { return true; } // Return false if dataset has 0 files and user want to 'publish' or 'submit for review' and 'publish dataset requires files' flag is set @@ -329,8 +331,24 @@ public boolean authUsersCanCreateDatasetsInDataverse(Dataverse dataverse) { public boolean authUsersCanCreateDataversesInDataverse(Dataverse dataverse) { return authenticatedUsersCanIssueCommand(dataverse, CreateDataverseCommand.class); } - - // todo: move any calls to this to call NavigationWrapper + + public boolean isPowerUserOn(DvObject dvo) { + User u = session.getUser(); + return (u instanceof AuthenticatedUser && permissionService.isPowerUserOn((AuthenticatedUser) u, dvo)); + } + + public boolean isPowerUserOnSomeDvObject() { + User u = session.getUser(); + if (u.isSuperuser()) { + return true; + } + if (!(u instanceof AuthenticatedUser)) { + return false; + } + return permissionService.isPowerUserOnSomeDvObject((AuthenticatedUser) u); + } + + // todo: move any calls to this to call NavigationWrapper @Inject NavigationWrapper navigationWrapper; public String notAuthorized(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index b32b5ae8d49..2bb49170458 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -796,7 +796,7 @@ protected DatasetFieldType findDatasetFieldType(String idtf) throws NumberFormat */ protected Response getRoleAssignmentHistoryResponse(DvObject dvObject, AuthenticatedUser authenticatedUser, boolean forFiles, HttpHeaders headers) { // Check if the user has permission to manage permissions for this object - if (!permissionSvc.userOn(authenticatedUser, dvObject).has(Permission.ManageDatasetPermissions)) { + if (!(permissionSvc.isPowerUserOn(authenticatedUser, dvObject) || permissionSvc.userOn(authenticatedUser, dvObject).has(Permission.ManageDatasetPermissions))) { return error(Status.FORBIDDEN, "You do not have permission to view the role assignment history for this " + dvObject.getClass().getSimpleName().toLowerCase()); } 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 cdb27fd5f21..b5e6f6556c4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -698,7 +698,7 @@ public String tabularDatafileMetadataDDI(@Context ContainerRequestContext crc, DataverseRequest req = createDataverseRequest(getRequestUser(crc)); dataFile = findDataFileUserCanSeeOrDieWrapper(fileId, req); - + // This will throw a ForbiddenException if access isn't authorized: checkAuthorization(req.getUser(), dataFile); @@ -1934,7 +1934,7 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, } - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { + if (!(permissionService.isPowerUserOn(dataverseRequest.getAuthenticatedUser(), dataFile) || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } @@ -2154,7 +2154,7 @@ public Response rejectFileAccess(@Context ContainerRequestContext crc, dataverseRequest = createDataverseRequest(getRequestUser(crc)); - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { + if (!(permissionService.isPowerUserOn(dataverseRequest.getAuthenticatedUser(), dataFile) || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } FileAccessRequest far = dataFile.getAccessRequestForAssignee(ra); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index e30be924903..af797ae768e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -20,7 +20,6 @@ import edu.harvard.iq.dataverse.settings.SettingsValidationException; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; -import edu.harvard.iq.dataverse.util.json.JsonPrinter; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.EMailValidator; @@ -1791,16 +1790,15 @@ public Response reregisterHdlToPID(@Context ContainerRequestContext crc, @PathParam("id") String id) { logger.info("Starting to reregister " + id + " Dataset Id. (from hdl to doi)" + new Date()); try { - - User u = getRequestUser(crc); - if (!u.isSuperuser()) { + Dataset ds = findDatasetOrDie(id); + + if (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) u, ds)) { logger.info("Bad Request Unauthor " ); - return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser")); + return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } DataverseRequest r = createDataverseRequest(u); - Dataset ds = findDatasetOrDie(id); if (HandlePidProvider.HDL_PROTOCOL.equals(dvObjectService.getEffectivePidGenerator(ds).getProtocol())) { logger.info("Bad Request protocol set to handle " ); @@ -1836,8 +1834,11 @@ public Response registerDataFile(@Context ContainerRequestContext crc, try { User u = getRequestUser(crc); - DataverseRequest r = createDataverseRequest(u); DataFile df = findDataFileOrDie(id); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, df))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + DataverseRequest r = createDataverseRequest(u); if(!systemConfig.isFilePIDsEnabledForCollection(df.getOwner().getOwner())) { return forbidden("PIDs are not enabled for this file's collection."); } @@ -1946,9 +1947,14 @@ public Response registerDataFilesInCollection(@Context ContainerRequestContext c return r.getResponse(); } - AuthenticatedUser superuser = authSvc.getAdminUser(); - if (superuser == null) { - return error(Response.Status.INTERNAL_SERVER_ERROR, "Cannot find the superuser to execute /admin/registerDataFiles."); + AuthenticatedUser user; + try { + user = getRequestAuthenticatedUserOrDie(crc); + if (!permissionSvc.isPowerUserOn(user, collection)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); } if (!systemConfig.isFilePIDsEnabledForCollection(collection)) { @@ -1978,7 +1984,7 @@ public Response registerDataFilesInCollection(@Context ContainerRequestContext c if ((df.getIdentifier() == null || df.getIdentifier().isEmpty())) { if (df.isReleased()) { countReleased++; - DataverseRequest r = createDataverseRequest(superuser); + DataverseRequest r = createDataverseRequest(user); execCommand(new RegisterDvObjectCommand(r, df)); countSuccesses++; if (countSuccesses % 100 == 0) { @@ -2141,16 +2147,14 @@ public Response computeDataFileHashValue(@Context ContainerRequestContext crc, @Parameter(description = "Checksum algorithm to calculate.", required = true) @PathParam("alg") String alg) { + AuthenticatedUser u; try { - User u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(Status.UNAUTHORIZED, "must be superuser"); - } + u = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse e1) { return error(Status.UNAUTHORIZED, "api key required"); } - DataFile fileToUpdate = null; + DataFile fileToUpdate; try { fileToUpdate = findDataFileOrDie(fileId); } catch (WrappedResponse r) { @@ -2158,6 +2162,10 @@ public Response computeDataFileHashValue(@Context ContainerRequestContext crc, return error(Status.BAD_REQUEST, "Could not find file with the id: " + fileId); } + if (!permissionSvc.isPowerUserOn(u, fileToUpdate)) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + if (fileToUpdate.isHarvested()) { return error(Status.BAD_REQUEST, "File with the id: " + fileId + " is harvested."); } @@ -2207,16 +2215,14 @@ public Response validateDataFileHashValue(@Context ContainerRequestContext crc, @Parameter(description = "Data file id or persistent identifier.", required = true) @PathParam("fileId") String fileId) { + AuthenticatedUser u; try { - User u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(Status.UNAUTHORIZED, "must be superuser"); - } + u = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse e1) { return error(Status.UNAUTHORIZED, "api key required"); } - DataFile fileToValidate = null; + DataFile fileToValidate; try { fileToValidate = findDataFileOrDie(fileId); } catch (WrappedResponse r) { @@ -2224,6 +2230,10 @@ public Response validateDataFileHashValue(@Context ContainerRequestContext crc, return error(Status.BAD_REQUEST, "Could not find file with the id: " + fileId); } + if (!permissionSvc.isPowerUserOn(u, fileToValidate)) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + if (fileToValidate.isHarvested()) { return error(Status.BAD_REQUEST, "File with the id: " + fileId + " is harvested."); } @@ -2475,8 +2485,8 @@ public Response addRoleAssignementsToChildren(@Context ContainerRequestContext c AuthenticatedUser user = null; try { user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, owner)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2500,7 +2510,7 @@ public Response addRoleAssignementsToChildren(@Context ContainerRequestContext c @AuthRequired @Path("/dataverse/{alias}/curationLabelSet") @Operation(summary = "Returns a dataverse curation label set", - description = "Returns the curation label set configured on a dataverse when the requester is a superuser.") + description = "Returns the curation label set configured on a dataverse when the requester is a power user.") public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Parameter(description = "Dataverse alias.") @PathParam("alias") String alias) throws WrappedResponse { Dataverse dataverse = dataverseSvc.findByAlias(alias); if (dataverse == null) { @@ -2508,8 +2518,8 @@ public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Param } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2536,8 +2546,8 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2571,8 +2581,8 @@ public Response resetCurationLabelSet(@Context ContainerRequestContext crc, } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); 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 1b549b10a6b..298d97388d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -341,8 +341,8 @@ public Response deleteDataset(@Context ContainerRequestContext crc, @Parameter(d boolean destroy = false; if (doomed.getVersions().size() == 1) { - if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete published datasets")); + if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) u, doomed))) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"))); } destroy = true; } else { @@ -379,11 +379,11 @@ public Response destroyDataset(@Context ContainerRequestContext crc, @Parameter( User u = getRequestUser(crc); return response(req -> { - // first check if dataset is released, and if so, if user is a superuser + // first check if dataset is released, and if so, if user is a poweruser Dataset doomed = findDatasetOrDie(id); - if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Destroy can only be called by superusers.")); + if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) u, doomed))) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"))); } // Gather the locations of the physical files that will need to be @@ -1368,6 +1368,8 @@ public Response publishDataset(@Context ContainerRequestContext crc, } boolean updateCurrent=false; AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); + Dataset ds = findDatasetOrDie(id); + type = type.toLowerCase(); boolean isMinor=false; switch (type) { @@ -1378,17 +1380,17 @@ public Response publishDataset(@Context ContainerRequestContext crc, isMinor = false; break; case "updatecurrent": - if (user.isSuperuser()) { + if (permissionSvc.isPowerUserOn(user, ds)) { updateCurrent = true; } else { - return error(Response.Status.FORBIDDEN, "Only superusers can update the current version"); + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } break; default: return error(Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major', 'minor', or 'updatecurrent'."); } - Dataset ds = findDatasetOrDie(id); + boolean hasValidTerms = TermsOfUseAndAccessValidator.isTOUAValid(ds.getLatestVersion().getTermsOfUseAndAccess(), null); if (!hasValidTerms) { @@ -1498,18 +1500,17 @@ public Response publishDataset(@Context ContainerRequestContext crc, @Path("{id}/actions/:releasemigrated") @Consumes("application/ld+json, application/json-ld") @Operation(summary = "Publishes a migrated dataset", - description = "Publishes a migrated dataset using supplied JSON-LD publication metadata when the requester is a superuser.") + description = "Publishes a migrated dataset using supplied JSON-LD publication metadata when the requester is a poweruser.") @RequestBody(description = "JSON-LD metadata containing the migrated dataset publication date.") public Response publishMigratedDataset(@Context ContainerRequestContext crc, @RequestBody(description = "JSON-LD metadata containing the migrated dataset publication date.") String jsonldBody, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id, @DefaultValue("false") @Parameter(description = "Whether PID provider metadata should be updated.") @QueryParam ("updatepidatprovider") boolean contactPIDProvider) { try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Only superusers can release migrated datasets"); - } - Dataset ds = findDatasetOrDie(id); + if (!permissionSvc.isPowerUserOn(user, ds)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } try { JsonObject metadata = JSONLDUtil.decontextualizeJsonLD(jsonldBody); String pubDate = metadata.getString(JsonLDTerm.schemaOrg("datePublished").getUrl()); @@ -1661,8 +1662,8 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet * check later. */ - if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { - return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + if ((!permissionSvc.isPowerUserOn(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or have ScopeedPowerUser permission, or user does not have EditDataset permissions"); } // check if embargoes are allowed(:MaxEmbargoDurationInMonths), gets the :MaxEmbargoDurationInMonths setting variable, if 0 or not set(null) return 400 @@ -1739,9 +1740,10 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet if (datasetFiles.containsAll(filesToEmbargo)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : filesToEmbargo) { - // superuser can overrule an existing embargo, even on released files - if (datafile.isReleased() && !authenticatedUser.isSuperuser()) { + // power users can overrule an existing embargo, even on released files + if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -1775,8 +1777,8 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet embargoService.deleteById(emb.getId(), authenticatedUser.getIdentifier()); } } - //If superuser, report changes to any released files - if (authenticatedUser.isSuperuser()) { + //If poweruser, report changes to any released files + if (permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { String releasedFiles = filesToEmbargo.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); if (!releasedFiles.isBlank()) { @@ -1820,11 +1822,10 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet return ex.getResponse(); } - // client is superadmin or (client has EditDataset permission on these files and files are unreleased) // check if files are unreleased(DRAFT?) //ToDo - here and below - check the release status of files and not the dataset state (draft dataset version still can have released files) - if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { - return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + if ((!permissionSvc.isPowerUserOn(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or have ScopedPowerUser permission, or user does not have EditDataset permissions"); } // check if embargoes are allowed(:MaxEmbargoDurationInMonths), gets the :MaxEmbargoDurationInMonths setting variable, if 0 or not set(null) return 400 @@ -1865,9 +1866,10 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet if (datasetFiles.containsAll(embargoFilesToUnset)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : embargoFilesToUnset) { - // superuser can overrule an existing embargo, even on released files - if (datafile.getEmbargo()==null || ((datafile.isReleased() && datafile.getEmbargo() != null) && !authenticatedUser.isSuperuser())) { + // power users can overrule an existing embargo, even on released files + if (datafile.getEmbargo()==null || ((datafile.isReleased() && datafile.getEmbargo() != null) && !isPowerUser)) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -1943,8 +1945,8 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param // client is superadmin or (client has EditDataset permission on these files and files are unreleased) // check if files are unreleased(DRAFT?) - if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { - return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + if ((!permissionSvc.isPowerUserOn(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or have ScopedPowerUser permission, or user does not have EditDataset permissions"); } // check if retentions are allowed(:MinRetentionDurationInMonths), gets the :MinRetentionDurationInMonths setting variable, if 0 or not set(null) return 400 @@ -2041,9 +2043,10 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param if (datasetFiles.containsAll(filesToRetention)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : filesToRetention) { - // superuser can overrule an existing retention, even on released files - if (datafile.isReleased() && !authenticatedUser.isSuperuser()) { + // power users can overrule an existing retention, even on released files + if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -2077,8 +2080,8 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param retentionService.delete(ret, authenticatedUser.getIdentifier()); } } - //If superuser, report changes to any released files - if (authenticatedUser.isSuperuser()) { + //If power user, report changes to any released files + if (isPowerUser) { String releasedFiles = filesToRetention.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); if (!releasedFiles.isBlank()) { @@ -2123,8 +2126,8 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @Param // client is superadmin or (client has EditDataset permission on these files and files are unreleased) // check if files are unreleased(DRAFT?) //ToDo - here and below - check the release status of files and not the dataset state (draft dataset version still can have released files) - if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { - return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + if ((!permissionSvc.isPowerUserOn(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser/power user or user does not have EditDataset permissions"); } // check if retentions are allowed(:MinRetentionDurationInMonths), gets the :MinRetentionDurationInMonths setting variable, if 0 or not set(null) return 400 @@ -2176,9 +2179,10 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @Param if (datasetFiles.containsAll(retentionFilesToUnset)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : retentionFilesToUnset) { - // superuser can overrule an existing retention, even on released files - if (datafile.getRetention()==null || ((datafile.isReleased() && datafile.getRetention() != null) && !authenticatedUser.isSuperuser())) { + // superuser/power user can overrule an existing retention, even on released files + if (datafile.getRetention()==null || ((datafile.isReleased() && datafile.getRetention() != null) && !isPowerUser)) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -2225,7 +2229,7 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @Param @AuthRequired @Path("{id}/files/uploadlimit/{limit}") @Operation(summary = "Sets dataset file upload limit", - description = "Sets the file count upload limit for a dataset when the requester is a superuser.") + description = "Sets the file count upload limit for a dataset when the requester is a poweruser.") public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id, @Parameter(description = "Maximum number of results or configured limit value.") @PathParam("limit") int datasetFileCountLimit) { @@ -2237,9 +2241,6 @@ public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, } catch (WrappedResponse ex) { return error(Status.UNAUTHORIZED, "Authentication is required."); } - if (!authenticatedUser.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; try { @@ -2248,6 +2249,10 @@ public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setDatasetFileCountLimit(datasetFileCountLimit); datasetService.merge(dataset); return ok("ok"); @@ -2257,7 +2262,7 @@ public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, @AuthRequired @Path("{id}/files/uploadlimit") @Operation(summary = "Clears dataset file upload limit", - description = "Removes the file count upload limit from a dataset when the requester is a superuser.") + description = "Removes the file count upload limit from a dataset when the requester is a power user.") public Response deleteDatasetFilesLimits(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id) { // user is authenticated @@ -2267,9 +2272,6 @@ public Response deleteDatasetFilesLimits(@Context ContainerRequestContext crc, } catch (WrappedResponse ex) { return error(Status.UNAUTHORIZED, "Authentication is required."); } - if (!authenticatedUser.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; try { @@ -2278,6 +2280,10 @@ public Response deleteDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setDatasetFileCountLimit(null); datasetService.merge(dataset); return ok("ok"); @@ -2725,12 +2731,12 @@ public Response receiveChecksumValidationResults(@Context ContainerRequestContex } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!authenticatedUser.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } String statusMessageFromDcm = jsonFromDcm.getString("status"); try { Dataset dataset = findDatasetOrDie(id); + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); + } if ("validation passed".equals(statusMessageFromDcm)) { logger.log(Level.INFO, "Checksum Validation passed for DCM."); @@ -3025,7 +3031,7 @@ public Response getMPUploadUrls(@Context ContainerRequestContext crc, @Parameter return error(Response.Status.NOT_FOUND, "Direct upload not supported for files in this dataset: " + dataset.getId()); } - if (!user.isSuperuser()) { + if (!(user instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataset)) { Integer effectiveDatasetFileCountLimit = dataset.getEffectiveDatasetFileCountLimit(); boolean hasFileCountLimit = dataset.isDatasetFileCountLimitSet(effectiveDatasetFileCountLimit); if (hasFileCountLimit) { @@ -3643,10 +3649,10 @@ public Response deleteLocks(@Context ContainerRequestContext crc, @Parameter(des return response(req -> { try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); - } Dataset dataset = findDatasetOrDie(id); + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } if (lockType == null) { Set locks = new HashSet<>(); @@ -3695,10 +3701,10 @@ public Response lockDataset(@Context ContainerRequestContext crc, @Parameter(des return response(req -> { try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); - } Dataset dataset = findDatasetOrDie(id); + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } DatasetLock lock = dataset.getLockFor(lockType); if (lock != null) { return error(Response.Status.FORBIDDEN, "dataset already locked with lock type " + lockType); @@ -4014,32 +4020,31 @@ public Response getFileStore(@Context ContainerRequestContext crc, @Parameter(de @AuthRequired @Path("{identifier}/storageDriver") @Operation(summary = "Sets the dataset storage driver", - description = "Sets a dataset-specific storage driver by label when the requester is a superuser.") + description = "Sets a dataset-specific storage driver by label when the requester is a power user.") @RequestBody(description = "Storage driver label to assign to the dataset.") public Response setFileStore(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @RequestBody(description = "Storage driver label to assign to the dataset.") String storageDriverLabel, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + // We don't want to allow setting this to a store id that does not exist: for (Entry store : DataAccess.getStorageDriverLabels().entrySet()) { if (store.getKey().equals(storageDriverLabel)) { @@ -4060,25 +4065,24 @@ public Response setFileStore(@Context ContainerRequestContext crc, @Parameter(de public Response resetFileStore(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setStorageDriverId(null); datasetService.merge(dataset); return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); @@ -4088,28 +4092,21 @@ public Response resetFileStore(@Context ContainerRequestContext crc, @Parameter( @AuthRequired @Path("{identifier}/curationLabelSet") @Operation(summary = "Returns the dataset curation label set", - description = "Returns the effective curation label set name for a dataset when the requester is a superuser.") + description = "Returns the effective curation label set name for a dataset when the requester is a power user.") public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - AuthenticatedUser user = null; - try { - user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } - } catch (WrappedResponse wr) { - return wr.getResponse(); - } Dataset dataset; - try { + AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); DataverseRequest req = createDataverseRequest(user); dataset = findDatasetUserCanSeeOrDie(dvIdtf, req,false); + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } } catch (WrappedResponse ex) { return ex.getResponse(); } - return response(req -> ok(dataset.getEffectiveCurationLabelSetName()), getRequestUser(crc)); } @@ -4117,31 +4114,30 @@ public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Param @AuthRequired @Path("{identifier}/curationLabelSet") @Operation(summary = "Sets the dataset curation label set", - description = "Sets the curation label set for a dataset when the requester is a superuser.") + description = "Sets the curation label set for a dataset when the requester is a power user.") public Response setCurationLabelSet(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Parameter(description = "Curation label set name.") @QueryParam("name") String curationLabelSet, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.UNAUTHORIZED, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return ex.getResponse(); } + + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } if (SystemConfig.CURATIONLABELSDISABLED.equals(curationLabelSet) || SystemConfig.DEFAULTCURATIONLABELSET.equals(curationLabelSet)) { dataset.setCurationLabelSetName(curationLabelSet); datasetService.merge(dataset); @@ -4167,25 +4163,24 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, public Response resetCurationLabelSet(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return ex.getResponse(); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setCurationLabelSetName(SystemConfig.DEFAULTCURATIONLABELSET); datasetService.merge(dataset); return ok("Curation Label Set reset to default: " + SystemConfig.DEFAULTCURATIONLABELSET); @@ -5388,7 +5383,7 @@ public Response getCurationStates(@Context ContainerRequestContext crc, @Produces(MediaType.APPLICATION_JSON) @Path("/{id}/{version}/archivalStatus") @Operation(summary = "Returns archival status for a dataset version", - description = "Returns stored archival status information for a dataset version when the requester is a superuser.") + description = "Returns stored archival status information for a dataset version when the requester is a power user.") public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String datasetId, @Parameter(description = "Dataset version selector.") @PathParam("version") String versionNumber, @@ -5397,13 +5392,13 @@ public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); - if (!au.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } DataverseRequest req = createDataverseRequest(au); - DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetUserCanSeeOrDie(datasetId, req, false), uriInfo, + Dataset ds = findDatasetUserCanSeeOrDie(datasetId, req, false); + if (!permissionSvc.isPowerUserOn(au, ds)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, true); - if (dsv.getArchivalCopyLocation() == null) { return error(Status.NOT_FOUND, "This dataset version has not been archived"); } else { @@ -5420,7 +5415,7 @@ public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext @Consumes(MediaType.APPLICATION_JSON) @Path("/{id}/{version}/archivalStatus") @Operation(summary = "Sets dataset version archival status", - description = "Sets the archival status payload for a dataset version when the requester is a superuser.") + description = "Sets the archival status payload for a dataset version when the requester is a power user.") @RequestBody(description = "JSON archival status payload to store for the dataset version.") public Response setDatasetVersionArchivalStatus(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String datasetId, @@ -5433,9 +5428,9 @@ public Response setDatasetVersionArchivalStatus(@Context ContainerRequestContext logger.fine(newStatus); try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); - - if (!au.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + Dataset dataset = findDatasetOrDie(datasetId); + if (!permissionSvc.isPowerUserOn(au, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } //Verify we have valid json after removing any HTML tags (the status gets displayed in the UI, so we want plain text). @@ -5447,7 +5442,7 @@ public Response setDatasetVersionArchivalStatus(@Context ContainerRequestContext || status.equals(DatasetVersion.ARCHIVAL_STATUS_SUCCESS)) { DataverseRequest req = createDataverseRequest(au); - DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), + DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, dataset, uriInfo, headers, true); if (dsv == null) { @@ -5482,7 +5477,7 @@ public Response setDatasetVersionArchivalStatus(@Context ContainerRequestContext @Produces(MediaType.APPLICATION_JSON) @Path("/{id}/{version}/archivalStatus") @Operation(summary = "Deletes archival status for a dataset version", - description = "Removes stored archival status information from a dataset version when the requester is a superuser.") + description = "Removes stored archival status information from a dataset version when the requester is a power user.") public Response deleteDatasetVersionArchivalStatus(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String datasetId, @Parameter(description = "Dataset version selector.") @PathParam("version") String versionNumber, @@ -5491,12 +5486,12 @@ public Response deleteDatasetVersionArchivalStatus(@Context ContainerRequestCont try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); - if (!au.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + Dataset ds = findDatasetOrDie(datasetId); + if (!permissionSvc.isPowerUserOn(au, ds)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } - DataverseRequest req = createDataverseRequest(au); - DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), uriInfo, + DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, true); if (dsv == null) { return error(Status.NOT_FOUND, "Dataset version not found"); @@ -5925,31 +5920,30 @@ public Response getGuestbookEntryOption(@Context ContainerRequestContext crc, @P @AuthRequired @Path("{identifier}/guestbookEntryAtRequest") @Operation(summary = "Sets the dataset guestbook-entry policy", - description = "Sets whether a dataset requires guestbook entry at request time when the requester is a superuser.") + description = "Sets whether a dataset requires guestbook entry at request time when the requester is a power user.") @RequestBody(description = "Boolean guestbook-entry-at-request value to store for the dataset.") public Response setguestbookEntryAtRequest(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @RequestBody(description = "Boolean guestbook-entry-at-request value to store for the dataset.") boolean gbAtRequest, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } Optional gbAtRequestOpt = JvmSettings.GUESTBOOK_AT_REQUEST.lookupOptional(Boolean.class); if (!gbAtRequestOpt.isPresent()) { return error(Response.Status.FORBIDDEN, "Guestbook Entry At Request cannot be set. This server is not configured to allow it."); @@ -5968,25 +5962,24 @@ public Response setguestbookEntryAtRequest(@Context ContainerRequestContext crc, public Response resetGuestbookEntryAtRequest(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setGuestbookEntryAtRequest(DvObjectContainer.UNDEFINED_CODE); datasetService.merge(dataset); return ok("Guestbook Entry At Request reset to default: " + dataset.getEffectiveGuestbookEntryAtRequest()); @@ -6037,27 +6030,23 @@ public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext cr @AuthRequired @Path("{identifier}/pidReconcile") @Operation(summary = "Reconciles a dataset persistent identifier", - description = "Reconciles a dataset persistent identifier with its effective PID provider when the requester is a superuser.") + description = "Reconciles a dataset persistent identifier with its effective PID provider when the requester is a power user.") public Response reconcilePid(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String datasetId) throws WrappedResponse { - - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.UNAUTHORIZED, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } - Dataset dataset; - PidProvider pidProvider; try { dataset = findDatasetOrDie(datasetId); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } return response(req -> { execCommand(new ReconcileDatasetPidCommand(req, dataset, dataset.getEffectivePidGenerator())); return ok(dataset.getGlobalId().toString()); @@ -6099,30 +6088,29 @@ public Response getPidGenerator(@Context ContainerRequestContext crc, @Parameter @AuthRequired @Path("{identifier}/pidGenerator") @Operation(summary = "Sets the dataset PID generator", - description = "Sets a managed PID generator for a dataset when the requester is a superuser.") + description = "Sets a managed PID generator for a dataset when the requester is a power user.") @RequestBody(description = "Managed PID generator id to assign to the dataset.") public Response setPidGenerator(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String datasetId, @RequestBody(description = "Managed PID generator id to assign to the dataset.") String generatorId, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.UNAUTHORIZED, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(datasetId); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } if (PidUtil.getManagedProviderIds().contains(generatorId)) { dataset.setPidGeneratorId(generatorId); datasetService.merge(dataset); @@ -6141,25 +6129,24 @@ public Response setPidGenerator(@Context ContainerRequestContext crc, @Parameter public Response resetPidGenerator(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.") @PathParam("identifier") String dvIdtf, @Context HttpHeaders headers) throws WrappedResponse { - // Superuser-only: AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse ex) { return error(Response.Status.BAD_REQUEST, "Authentication is required."); } - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } + if (!permissionSvc.isPowerUserOn(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setPidGenerator(null); datasetService.merge(dataset); return ok("Pid Generator reset to default: " + dataset.getEffectivePidGenerator().getId()); @@ -6619,7 +6606,7 @@ public Response getVersionCreationNote(@Context ContainerRequestContext crc, @AuthRequired @Path("{id}/versions/{versionId}/versionNote") @Operation(summary = "Store a dataset version note", - description = "Adds or replaces the note on a draft or superuser-selected published dataset version.") + description = "Adds or replaces the note on a draft or power-user-selected published dataset version.") public Response addVersionNote(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.", required = true) @PathParam("id") String datasetId, @@ -6634,12 +6621,12 @@ public Response addVersionNote(@Context ContainerRequestContext crc, if (!DS_VERSION_DRAFT.equals(versionId)) { try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - - if (!user.isSuperuser()) { - return forbidden(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.forbidden")); + Dataset ds = findDatasetOrDie(datasetId); + if (!permissionSvc.isPowerUserOn(user, ds)) { + return forbidden(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } return response(req -> { - DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, ds, uriInfo, headers); datasetVersion.setVersionNote(note); execCommand(new UpdatePublishedDatasetVersionCommand(req, datasetVersion)); return ok("Note added to version " + datasetVersion.getFriendlyVersionNumber()); @@ -6661,7 +6648,7 @@ public Response addVersionNote(@Context ContainerRequestContext crc, @AuthRequired @Path("{id}/versions/{versionId}/versionNote") @Operation(summary = "Clear a dataset version note", - description = "Deletes the note from a draft or superuser-selected published dataset version.") + description = "Deletes the note from a draft or power-user-selected published dataset version.") public Response deleteVersionNote(@Context ContainerRequestContext crc, @Parameter(description = "Dataset id or persistent identifier.", required = true) @PathParam("id") String datasetId, @@ -6673,8 +6660,9 @@ public Response deleteVersionNote(@Context ContainerRequestContext crc, } if (!DS_VERSION_DRAFT.equals(versionId)) { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return forbidden(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.forbidden")); + Dataset ds = findDatasetOrDie(datasetId); + if (!permissionSvc.isPowerUserOn(user, ds)) { + return forbidden(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } return response(req -> { 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 8e4a83f48b9..05deb0d7061 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -65,7 +65,6 @@ import jakarta.ws.rs.core.Response.Status; import java.text.MessageFormat; -import java.text.SimpleDateFormat; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.WebApplicationException; @@ -428,8 +427,8 @@ public Response createDataset(@Context ContainerRequestContext crc, return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.mustIncludeVersion")); } - if (!ds.getFiles().isEmpty() && !u.isSuperuser()){ - return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.superuserFiles")); + if (!ds.getFiles().isEmpty() && !(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))){ + return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.powerUserFiles")); } //Throw BadRequestException if metadataLanguage isn't compatible with setting @@ -553,10 +552,10 @@ public Response importDataset(@Context ContainerRequestContext crc, @QueryParam("release") String releaseParam) { try { User u = getRequestUser(crc); - if (!u.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } Dataverse owner = findDataverseOrDie(parentIdtf); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } Dataset ds = parseDataset(jsonBody, owner); if (ds.getVersions().isEmpty()) { @@ -643,10 +642,10 @@ public Response importDatasetDdi(@Context ContainerRequestContext crc, @QueryParam("release") String releaseParam) { try { User u = getRequestUser(crc); - if (!u.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } Dataverse owner = findDataverseOrDie(parentIdtf); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } Dataset ds; try { ds = jsonParser().parseDataset(importService.ddiToJson(xml), owner); @@ -724,11 +723,11 @@ public Response recreateDataset(@Context ContainerRequestContext crc, @PathParam("identifier") String parentIdtf) { try { User u = getRequestUser(crc); - if (!u.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } Dataverse owner = findDataverseOrDie(parentIdtf); - + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } + Dataset ds = new Dataset(); ds.setOwner(owner); @@ -1935,8 +1934,8 @@ public Response listLinks(@Context ContainerRequestContext crc, User u = getRequestUser(crc); DataverseRequest req = createDataverseRequest(u); Dataverse dv = findDataverseUserCanSeeOrDie(dvIdtf, req); - if (!u.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, dv))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } List dvsThisDvHasLinkedToList = dataverseSvc.findDataversesThisIdHasLinkedTo(dv.getId()); @@ -2641,8 +2640,8 @@ public Response setStorageDriver(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, dataverse)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2669,8 +2668,8 @@ public Response resetStorageDriver(@Context ContainerRequestContext crc, Dataverse dataverse = findDataverseOrDie(id); try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUserOn(user, dataverse)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2716,11 +2715,10 @@ public Response listLocallyFairRoleAssignees(@Context ContainerRequestContext cr @PathParam("identifier") String dvIdtf) { try { User user = getRequestUser(crc); - if (!user.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } - Dataverse dataverse = findDataverseOrDie(dvIdtf); + if (!(user instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataverse))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } JsonArrayBuilder assignees = JsonUtil.createArrayBuilder(); dataverse.getLocallyFAIRRoleAssigneeIdentifiers().stream() .sorted() @@ -2746,11 +2744,10 @@ public Response setLocallyFairRoleAssignees(@Context ContainerRequestContext crc List roleAssigneeIdentifiers) { try { User user = getRequestUser(crc); - if (!user.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } - Dataverse dataverse = findDataverseOrDie(dvIdtf); + if (!(user instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataverse))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } Set validatedIdentifiers = validateLocallyFairRoleAssigneeIdentifiers(roleAssigneeIdentifiers); dataverse.setLocallyFAIRRoleAssigneeIdentifiers(validatedIdentifiers); dataverseService.save(dataverse); @@ -2775,11 +2772,10 @@ public Response addLocallyFairRoleAssignee(@Context ContainerRequestContext crc, @PathParam("roleAssigneeIdentifier") String roleAssigneeIdentifier) { try { User user = getRequestUser(crc); - if (!user.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } - Dataverse dataverse = findDataverseOrDie(dvIdtf); + if (!(user instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataverse))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } if (findAssignee(roleAssigneeIdentifier) == null) { return badRequest("Invalid role assignee identifier: " + roleAssigneeIdentifier); } @@ -2807,11 +2803,10 @@ public Response deleteLocallyFairRoleAssignee(@Context ContainerRequestContext c @PathParam("roleAssigneeIdentifier") String roleAssigneeIdentifier) { try { User user = getRequestUser(crc); - if (!user.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); - } - Dataverse dataverse = findDataverseOrDie(dvIdtf); + if (!(user instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataverse))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } if(StringUtils.isBlank(roleAssigneeIdentifier) || !dataverse.getLocallyFAIRRoleAssigneeIdentifiers().contains(roleAssigneeIdentifier)) { return badRequest("Invalid role assignee identifier: " + roleAssigneeIdentifier); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 458faf790ec..43973e2f990 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -42,7 +42,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -61,7 +60,6 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; @@ -701,7 +699,7 @@ public Response getFileMetadataDraft(@Context ContainerRequestContext crc, @Para @AuthRequired @Path("{id}/uningest") @Operation(summary = "Uningests a data file", - description = "Converts an ingested tabular data file back to a regular file when the requester is a superuser.") + description = "Converts an ingested tabular data file back to a regular file when there was a failure or the requester is a power user.") public Response uningestDatafile(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id) { DataFile dataFile; @@ -761,19 +759,15 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @Paramete @AuthRequired @Path("{id}/reingest") @Operation(summary = "Reingests a data file", - description = "Starts ingest processing for a data file when the requester is a superuser.") + description = "Starts ingest processing for a data file when the requester is a power user.") public Response reingest(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id) { - AuthenticatedUser u; try { u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(FORBIDDEN, "This API call can be used by superusers only"); - } } catch (WrappedResponse wr) { return wr.getResponse(); } - + DataFile dataFile; try { dataFile = findDataFileOrDie(id); @@ -781,6 +775,10 @@ public Response reingest(@Context ContainerRequestContext crc, @Parameter(descri return error(Response.Status.NOT_FOUND, "File not found for given id."); } + if (!permissionSvc.isPowerUserOn(u, dataFile)) { + return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + Dataset dataset = dataFile.getOwner(); if (dataset == null) { @@ -862,16 +860,14 @@ public Response redetectDatafile(@Context ContainerRequestContext crc, @AuthRequired @Path("{id}/extractNcml") @Operation(summary = "Extracts NcML metadata", - description = "Extracts NcML metadata from a data file when the requester is a superuser.") + description = "Extracts NcML metadata from a data file when the requester is a power user.") public Response extractNcml(@Context ContainerRequestContext crc, @Parameter(description = "Resource id or persistent identifier.") @PathParam("id") String id) { try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); - if (!au.isSuperuser()) { - // We can always make a command in the future if there's a need - // for non-superusers to call this API. - return error(FORBIDDEN, "This API call can be used by superusers only"); - } DataFile dataFileIn = findDataFileOrDie(id); + if (!permissionSvc.isPowerUserOn(au, dataFileIn)) { + return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } java.nio.file.Path tempLocationPath = null; boolean successOrFail = ingestService.extractMetadataNcml(dataFileIn, tempLocationPath); NullSafeJsonBuilder result = NullSafeJsonBuilder.jsonObjectBuilder() diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index 33549608331..d611d35e8ee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -17,15 +17,13 @@ import edu.harvard.iq.dataverse.util.json.JsonParseException; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import edu.harvard.iq.dataverse.util.json.JsonUtil; -import jakarta.json.JsonObjectBuilder; + import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import java.io.IOException; -import java.io.StringReader; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import jakarta.ejb.EJB; -import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.ws.rs.DELETE; @@ -179,15 +177,6 @@ public Response createHarvestingClient(@Context ContainerRequestContext crc, @QueryParam("key") String apiKey) throws IOException, JsonParseException { // Per the discussion during the QA of PR #9174, we decided to make // the create/edit APIs superuser-only (the delete API was already so) - try { - User u = getRequestUser(crc); - if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can create harvesting clients.")); - } - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - try { JsonObject json = JsonUtil.getJsonObject(jsonBody); @@ -223,6 +212,11 @@ public Response createHarvestingClient(@Context ContainerRequestContext crc, if (ownerDataverse == null) { return error(Response.Status.BAD_REQUEST, "No such dataverse: " + dataverseAlias); } + + User u = getRequestUser(crc); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, ownerDataverse))) { + return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can create harvesting clients."); + } // The nickname supplied as part of the Rest path takes precedence: harvestingClient.setName(nickName); @@ -272,29 +266,16 @@ public Response modifyHarvestingClient(@Context ContainerRequestContext crc, @QueryParam("key") String apiKey) throws IOException, JsonParseException { try { User u = getRequestUser(crc); - if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can modify harvesting clients.")); + HarvestingClient harvestingClient = harvestingClientService.findByNickname(nickName); + if (harvestingClient == null) { + return error( Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); } - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - - HarvestingClient harvestingClient = null; - try { - harvestingClient = harvestingClientService.findByNickname(nickName); - } catch (Exception ex) { - // We don't care what happened; we'll just assume we couldn't find it. - harvestingClient = null; - } - - if (harvestingClient == null) { - return error( Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); - } - - String ownerDataverseAlias = harvestingClient.getDataverse().getAlias(); - - try { - DataverseRequest req = createDataverseRequest(getRequestUser(crc)); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, harvestingClient.getDataverse()))) { + return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can modify harvesting clients."); + } + + String ownerDataverseAlias = harvestingClient.getDataverse().getAlias(); + DataverseRequest req = createDataverseRequest(u); JsonObject json = JsonUtil.getJsonObject(jsonBody); HarvestingClient newHarvestingClient = new HarvestingClient(); @@ -361,48 +342,31 @@ public Response deleteHarvestingClient(@Context ContainerRequestContext crc, // harvested content associated with it). So instead of calling the command // directly, we will be calling an async. service bean method. - - try { - User u = getRequestUser(crc); - if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete harvesting clients.")); - } - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - - HarvestingClient harvestingClient = null; - - try { - harvestingClient = harvestingClientService.findByNickname(nickName); - } catch (Exception ex) { - logger.warning("Exception caught looking up harvesting client " + nickName + ": " + ex.getMessage()); - return error( Response.Status.BAD_REQUEST, "Internal error: failed to look up harvesting client " + nickName); - } - + User u = getRequestUser(crc); + HarvestingClient harvestingClient = harvestingClientService.findByNickname(nickName); if (harvestingClient == null) { return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); } - - // Check if the client is in a state where it can be safely deleted: - + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, harvestingClient.getDataverse()))) { + return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can delete harvesting clients."); + } + + // Check if the client is in a state where it can be safely deleted: if (harvestingClient.isDeleteInProgress()) { - return error( Response.Status.BAD_REQUEST, "Harvesting client " + nickName + " is already being deleted (in progress)"); + return error(Response.Status.BAD_REQUEST, "Harvesting client " + nickName + " is already being deleted (in progress)"); } - + if (harvestingClient.isHarvestingNow()) { - return error( Response.Status.BAD_REQUEST, "It is not safe to delete client " + nickName + " while a harvesting job is in progress"); + return error(Response.Status.BAD_REQUEST, "It is not safe to delete client " + nickName + " while a harvesting job is in progress"); } - - // Finally, delete it (asynchronously): - + + // Finally, delete it (asynchronously): try { harvestingClientService.deleteClient(harvestingClient.getId()); } catch (Exception ex) { - return error( Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); + return error(Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); } - - + return ok("Harvesting Client " + nickName + ": delete in progress"); } @@ -431,16 +395,16 @@ public Response startHarvestingJob(@Context ContainerRequestContext crc, return error(Response.Status.UNAUTHORIZED, "Authentication required to use this API method"); } - if (authenticatedUser == null || !authenticatedUser.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Only admin users can run harvesting jobs"); - } - HarvestingClient harvestingClient = harvestingClientService.findByNickname(clientNickname); if (harvestingClient == null) { return error(Response.Status.NOT_FOUND, "No such client: "+clientNickname); } + if (!permissionSvc.isPowerUserOn(authenticatedUser, harvestingClient.getDataverse())) { + return error(Response.Status.FORBIDDEN, "Only admin or power users can run harvesting jobs"); + } + DataverseRequest dataverseRequest = createDataverseRequest(authenticatedUser); harvesterService.doAsyncHarvest(dataverseRequest, harvestingClient); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Pids.java b/src/main/java/edu/harvard/iq/dataverse/api/Pids.java index db47de7bbed..8ea55d63326 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Pids.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Pids.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.impl.DeletePidCommand; import edu.harvard.iq.dataverse.engine.command.impl.ReservePidCommand; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/MediaResourceManagerImpl.java b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/MediaResourceManagerImpl.java index 5189963ae59..ab77cedba65 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/MediaResourceManagerImpl.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/MediaResourceManagerImpl.java @@ -10,7 +10,6 @@ import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.datasetutility.FileExceedsMaxSizeException; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; @@ -346,7 +345,7 @@ DepositReceipt replaceOrAddFiles(String uri, Deposit deposit, AuthCredentials au ConstraintViolation violation = constraintViolations.iterator().next(); throw new SwordError(UriRegistry.ERROR_BAD_REQUEST, "Unable to add file(s) to dataset: " + violation.getMessage() + " The invalid value was \"" + violation.getInvalidValue() + "\"."); } else { - boolean ignoreUploadFileLimits = user != null ? user.isSuperuser() : false; + boolean ignoreUploadFileLimits = user != null ? permissionService.isPowerUserOn(user, dataset) : false; ingestService.saveAndAddFilesToDataset(editVersion, dataFiles, null, true, ignoreUploadFileLimits); } } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java b/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java index ff1a5546f38..41f7c94a01c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java @@ -71,6 +71,7 @@ public class DataverseRole implements Serializable { public static final String MANAGER = "manager"; public static final String CURATOR = "curator"; public static final String MEMBER = "member"; + public static final String POWER_USER = "powerUser"; public static final String NONE = "none"; 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 77ff12fdee7..bd66ccc91b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java @@ -53,7 +53,8 @@ public enum Permission implements java.io.Serializable { 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); + DeleteDatasetDraft(BundleUtil.getStringFromBundle("permission.deleteDataset"), true, Dataset.class), + ScopedPowerUser(BundleUtil.getStringFromBundle("permission.scopedPowerUser"), true, DvObject.class); // FUTURE: diff --git a/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDatasetPage.java index a4023f41e24..8c2ae65c700 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDatasetPage.java @@ -107,7 +107,7 @@ public List completeSelectedDataverse(String query) { public String init() { - if ((session.getUser() != null) && (session.getUser().isAuthenticated()) && (session.getUser().isSuperuser())) { + if (permissionsWrapper.isPowerUserOnSomeDvObject()) { authUser = (AuthenticatedUser) session.getUser(); // initialize components, if any need it } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDataversePage.java b/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDataversePage.java index be3d05a823e..085f3628fb7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/dashboard/DashboardMoveDataversePage.java @@ -96,7 +96,7 @@ public List completeSelectedDataverse(String query) { public String init() { - if ((session.getUser() != null) && (session.getUser().isAuthenticated()) && (session.getUser().isSuperuser())) { + if (permissionsWrapper.isPowerUserOnSomeDvObject()) { authUser = (AuthenticatedUser) session.getUser(); // initialize components, if any need it } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index 03d91fb24d4..387225cf298 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -46,9 +46,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import jakarta.ejb.Asynchronous; import jakarta.ejb.EJBException; -import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonNumber; import jakarta.json.JsonObject; @@ -1594,7 +1592,7 @@ private boolean step_060_addFilesViaIngestService(boolean tabIngest){ } int nFiles = finalFileList.size(); - boolean ignoreUploadFileLimits = dvRequest.getAuthenticatedUser() != null ? dvRequest.getAuthenticatedUser().isSuperuser() : false; + boolean ignoreUploadFileLimits = dvRequest.getAuthenticatedUser() != null ? permissionService.isPowerUserOn(dvRequest.getAuthenticatedUser(), workingVersion.getDataset()) : false; finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace, tabIngest, ignoreUploadFileLimits); if (nFiles != finalFileList.size()) { @@ -2065,7 +2063,7 @@ public Response addFiles(String jsonData, Dataset dataset, User authUser, boolea workingVersion = dataset.getOrCreateEditVersion(); clone = workingVersion.cloneDatasetVersion(); - if (!authUser.isSuperuser()) { + if (!(authUser instanceof AuthenticatedUser) || !permissionService.isPowerUserOn((AuthenticatedUser) authUser, dataset)) { Integer effectiveDatasetFileCountLimit = dataset.getEffectiveDatasetFileCountLimit(); boolean hasFileCountLimit = dataset.isDatasetFileCountLimitSet(effectiveDatasetFileCountLimit); if (hasFileCountLimit) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java index 096238f5637..376047850af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java @@ -3,7 +3,6 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetFieldServiceBean; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetVersionDifference; import edu.harvard.iq.dataverse.DatasetVersionUser; @@ -31,7 +30,6 @@ import java.util.logging.Logger; import static java.util.stream.Collectors.joining; -import jakarta.ejb.EJB; import jakarta.validation.ConstraintViolation; import edu.harvard.iq.dataverse.settings.JvmSettings; @@ -321,8 +319,8 @@ protected void registerExternalVocabValuesIfAny(CommandContext ctxt, DatasetVers } // To block Publishing dataset or Submitting dataset for review - protected boolean getEffectiveRequiresFilesToPublishDataset() { - if (getUser().isSuperuser()) { + protected boolean getRequiresFilesToPublishOrReviewDataset(CommandContext ctxt) { + if (getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), getDataset())) { return false; } else { Dataverse dv = getDataset().getOwner(); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java index 3071cfaea8f..bf9263e669e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java @@ -55,7 +55,7 @@ protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandExcep throw new IllegalCommandException("Root Dataverse already exists. Cannot create another one", this); } } - if (!getUser().isSuperuser() && dataverse.isDatasetFileCountLimitSet(dataverse.getDatasetFileCountLimit())) { + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), owner)) && dataverse.isDatasetFileCountLimitSet(dataverse.getDatasetFileCountLimit())) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("file.dataset.error.set.file.count.limit"), this); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommand.java index 4a897adefa2..6afe9929d11 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommand.java @@ -35,8 +35,8 @@ public CreateRoleCommand(DataverseRole aRole, DataverseRequest aRequest, Dataver public DataverseRole execute(CommandContext ctxt) throws CommandException { User user = getUser(); //todo: temporary for 4.0 - only superusers can create and edit roles - if ((!(user instanceof AuthenticatedUser) || !user.isSuperuser())) { - throw new IllegalCommandException(BundleUtil.getStringFromBundle("permission.role.must.be.created.by.superuser"),this); + if (!(user instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) user, dv))) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("permission.role.must.be.created.by.poweruser"),this); } //Test to see if the role already exists in DB try { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java index 1c57a9d4647..c236400aad5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; @@ -52,8 +53,8 @@ public boolean isValidateLenient() { @Override public Dataset execute(CommandContext ctxt) throws CommandException { - if (!getUser().isSuperuser()) { - throw new IllegalCommandException("Only superusers can curate published dataset versions", this); + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), getDataset())) { + throw new IllegalCommandException("Only superusers or those with scopedPowerUser permission on the dataset can curate published dataset versions", this); } Dataset savedDataset = null; // Merge the dataset into our JPA context diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteCollectionQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteCollectionQuotaCommand.java index c0f863686da..be44d5e587a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteCollectionQuotaCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteCollectionQuotaCommand.java @@ -17,7 +17,7 @@ * * @author landreev * - * A superuser-only command: + * A power user-only command: */ @RequiredPermissions({}) public class DeleteCollectionQuotaCommand extends AbstractVoidCommand { @@ -33,9 +33,9 @@ public DeleteCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target) @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException(BundleUtil.getStringFromBundle("dataverse.storage.quota.superusersonly"), + // first check if user is a power user + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), targetDataverse) ) ) { + throw new PermissionException(BundleUtil.getStringFromBundle("dataverse.storage.quota.powerusersonly"), this, null, targetDataverse); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java index 0812c52a846..3730a5c128b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java @@ -54,8 +54,8 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { if (destroy) { //todo: clean this logic up! //for now, if called as destroy, will check for superuser acess - if (doomed.getOwner().isReleased() && (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException("Destroy can only be called by superusers.", + if (doomed.getOwner().isReleased() && (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), doomed))) { + throw new PermissionException("Destroy can only be called by superusers or users with ScopedPowerUser permission on the file.", this, Collections.singleton(Permission.DeleteDatasetDraft), doomed); } } else // since this is not a destroy, we want to make sure the file is a draft diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetQuotaCommand.java index 1ad7591e257..a8e39d1e63d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetQuotaCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetQuotaCommand.java @@ -17,7 +17,7 @@ * * @author landreev * - * A superuser-only command: + * A power-user-only command: */ @RequiredPermissions({}) public class DeleteDatasetQuotaCommand extends AbstractVoidCommand { @@ -33,9 +33,9 @@ public DeleteDatasetQuotaCommand(DataverseRequest aRequest, Dataset target) { @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.superusersonly"), + // first check if user is a power user + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), targetDataset) ) ) { + throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.powerusersonly"), this, null, targetDataset); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteHarvestingClientCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteHarvestingClientCommand.java deleted file mode 100644 index cff2e2e5540..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteHarvestingClientCommand.java +++ /dev/null @@ -1,57 +0,0 @@ -package edu.harvard.iq.dataverse.engine.command.impl; - -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; -import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; -import edu.harvard.iq.dataverse.engine.command.CommandContext; -import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; -import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; - -/** - * - * @author Leonid Andreev - */ -@RequiredPermissions( Permission.EditDataverse ) -public class DeleteHarvestingClientCommand extends AbstractVoidCommand { - - private final Dataverse motherDataverse; - private final HarvestingClient harvestingClient; - - public DeleteHarvestingClientCommand(DataverseRequest aRequest, HarvestingClient harvestingClient) { - super(aRequest, harvestingClient.getDataverse()); - this.motherDataverse = harvestingClient.getDataverse(); - this.harvestingClient = harvestingClient; - } - - @Override - public void executeImpl(CommandContext ctxt) throws CommandException { - - if (harvestingClient == null) { - throw new IllegalCommandException("DeleteHarvestingClientCommand: attempted to execute with null harvesting client; dataverse: "+motherDataverse.getAlias(), this); - } - - HarvestingClient merged = ctxt.em().merge(harvestingClient); - - // Purge all the SOLR documents associated with this client from the - // index server: - // ctxt.index().deleteHarvestedDocuments(merged); - - // All the datasets harvested by this client will be cleanly deleted - // through the defined cascade. Cascaded delete does not work for harvested - // files, however. So they need to be removed explicitly; before we - // proceed removing the client itself. - - for (DataFile harvestedFile : ctxt.files().findHarvestedFilesByClient(merged)) { - DataFile mergedFile = ctxt.em().merge(harvestedFile); - ctxt.em().remove(mergedFile); - harvestedFile = null; - } - - ctxt.em().remove(merged); - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java index e7350e63683..854aeedb105 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java @@ -36,8 +36,8 @@ public DeletePidCommand(DataverseRequest request, Dataset dataset) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { - throw new PermissionException(BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser"), this, + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataset)) { + throw new PermissionException(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"), this, Collections.singleton(Permission.EditDataset), dataset); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java index 49861e084b6..696d7a9ecb1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java @@ -69,9 +69,9 @@ public DestroyDatasetCommand(Dataset doomed, DataverseRequest aRequest) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - // first check if dataset is released, and if so, if user is a superuser - if ( doomed.isReleased() && (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException("Destroy can only be called by superusers.", + // first check if dataset is released, and if so, if user is a power user + if ( doomed.isReleased() && (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), doomed) ) ) { + throw new PermissionException("Destroy can only be called by superusers or users with ScopedPowerUser permission on the dataset.", this, Collections.singleton(Permission.DeleteDatasetDraft), doomed); } Dataset managedDoomed = ctxt.em().merge(doomed); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java index 231ab725188..803e7fa41ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; @@ -53,8 +54,8 @@ public ImportDatasetCommand(Dataset theDataset, DataverseRequest aRequest) { @Override protected void additionalParameterTests(CommandContext ctxt) throws CommandException { - if ( ! getUser().isSuperuser() ) { - throw new PermissionException("ImportDatasetCommand can only be issued by a super-user.", this, Collections.emptySet(), getDataset()); + if ( ! (getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), getDataset().getOwner())) ) { + throw new PermissionException("ImportDatasetCommand can only be issued by a super-user or power user.", this, Collections.emptySet(), getDataset()); } Dataset ds = getDataset(); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseContentCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseContentCommand.java index 26e1e988fac..0112937f34e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseContentCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseContentCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; @@ -32,7 +33,7 @@ public ListDataverseContentCommand(DataverseRequest aRequest, Dataverse anAffect @Override public List execute(CommandContext ctxt) throws CommandException { - if (getRequest().getUser().isSuperuser()) { + if (getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dvToList)) { return ctxt.dvObjects().findByOwnerId(dvToList.getId()); } else { return ctxt.permissions().whichChildrenHasPermissionsForOrReleased(getRequest(), dvToList, EnumSet.of(Permission.ViewUnpublishedDataverse, Permission.ViewUnpublishedDataset)); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java index a4e43da7dfa..cd1f4e8bd15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java @@ -64,13 +64,9 @@ public MoveDatasetCommand(DataverseRequest aRequest, Dataset moved, Dataverse de @Override public void executeImpl(CommandContext ctxt) throws CommandException { boolean removeGuestbook = false, removeLinkDs = false; - if (!(getUser() instanceof AuthenticatedUser)) { - /** - * This English wasn't moved to the bundle because it is impossible - * to exercise it via both API and UI. See also the note in in the - * PermissionException catch in AbstractApiBean. - */ - throw new PermissionException("Move Dataset can only be called by authenticated users.", this, Collections.singleton(Permission.DeleteDatasetDraft), moved); + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), moved))) { + throw new PermissionException(BundleUtil.getStringFromBundle("command.exception.only.powerusers", Arrays.asList(this.toString())), + this, Collections.singleton(Permission.DeleteDatasetDraft), moved); } // validate the move makes sense diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommand.java index c8b59b1818a..6dcc01bf659 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommand.java @@ -62,9 +62,9 @@ public void executeImpl(CommandContext ctxt) throws CommandException { logger.info("Starting dataverse move..."); boolean removeGuestbook = false, removeTemplate = false, removeFeatDv = false, removeMetadataBlock = false, removeLinkDv = false, removeLinkDs = false; - // first check if user is a superuser - if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException(BundleUtil.getStringFromBundle("command.exception.only.superusers", Arrays.asList(this.toString())), + // first check if user is a power user + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), moved))) { + throw new PermissionException(BundleUtil.getStringFromBundle("command.exception.only.powerusers", Arrays.asList(this.toString())), this, Collections.singleton(Permission.DeleteDataverse), moved); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java index df7cbc94c8d..510b2db2d64 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java @@ -96,7 +96,7 @@ public PublishDatasetResult execute(CommandContext ctxt) throws CommandException // Perform any optional validation steps, if defined: if (ctxt.systemConfig().isExternalDatasetValidationEnabled()) { // For admins, an override of the external validation step may be enabled: - if (!(getUser().isSuperuser() && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { + if (!(ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), theDataset) && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { String executable = ctxt.systemConfig().getDatasetValidationExecutable(); boolean result = validateDatasetMetadataExternally(theDataset, executable, getRequest()); @@ -233,7 +233,7 @@ private void verifyCommandArguments(CommandContext ctxt) throws IllegalCommandEx } List files = getDataset().getLatestVersion().getFileMetadatas(); - if ((files == null || files.isEmpty()) && getEffectiveRequiresFilesToPublishDataset()) { + if ((files == null || files.isEmpty()) && getRequiresFilesToPublishOrReviewDataset(ctxt)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.mayNotPublish.FilesRequired"), this); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDataverseCommand.java index d4b91f5940d..46f9dc6cee8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDataverseCommand.java @@ -12,7 +12,7 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; + import java.sql.Timestamp; import java.util.Date; import java.util.List; @@ -46,7 +46,7 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { // Perform any optional validation steps, if defined: if (ctxt.systemConfig().isExternalDataverseValidationEnabled()) { // For admins, an override of the external validation step may be enabled: - if (!(getUser().isSuperuser() && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { + if (!(ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataverse) && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { String executable = ctxt.systemConfig().getDataverseValidationExecutable(); boolean result = validateDataverseMetadataExternally(dataverse, executable, getRequest()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java index 77b06e4e152..4e46bd5e1a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java @@ -31,8 +31,8 @@ public ReservePidCommand(DataverseRequest request, Dataset dataset) { @Override public Dataset execute(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { - throw new PermissionException(BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser"), + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataset)) { + throw new PermissionException(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"), this, Collections.singleton(Permission.EditDataset), dataset); } registerExternalIdentifier(getDataset(), ctxt, true); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java index e52c47a5e7d..4c3901c2331 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java @@ -16,7 +16,7 @@ * * @author landreev * - * A superuser-only command: + * A power-user-only command: */ @RequiredPermissions({}) public class SetCollectionQuotaCommand extends AbstractVoidCommand { @@ -34,9 +34,9 @@ public SetCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target, Lo @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // Check if user is a superuser: - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException(BundleUtil.getStringFromBundle("dataverse.storage.quota.superusersonly"), + // Check if user is a power user: + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataverse) ) ) { + throw new PermissionException(BundleUtil.getStringFromBundle("dataverse.storage.quota.powerusersonly"), this, null, dataverse); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetDatasetQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetDatasetQuotaCommand.java index a9a44bfa347..b66fc1a42d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetDatasetQuotaCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetDatasetQuotaCommand.java @@ -16,7 +16,7 @@ * * @author landreev * - * A superuser-only command: + * A power-user-only command: */ @RequiredPermissions({}) public class SetDatasetQuotaCommand extends AbstractVoidCommand { @@ -34,9 +34,9 @@ public SetDatasetQuotaCommand(DataverseRequest aRequest, Dataset target, Long al @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // Check if user is a superuser: - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.superusersonly"), + // Check if user is a poweruser: + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn(getRequest().getAuthenticatedUser(), dataset) ) ) { + throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.powerusersonly"), this, null, dataset); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommand.java index 3ff53efa0e7..c8b229269cc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommand.java @@ -34,7 +34,7 @@ public Dataset execute(CommandContext ctxt) throws CommandException { } List files = getDataset().getLatestVersion().getFileMetadatas(); - if ((files == null || files.isEmpty()) && getEffectiveRequiresFilesToPublishDataset()) { + if ((files == null || files.isEmpty()) && getRequiresFilesToPublishOrReviewDataset(ctxt)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.mayNotSubmitForReview.FilesRequired"), this); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index ba04c4d7931..79c19d5b43a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java @@ -21,6 +21,7 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import java.io.IOException; @@ -47,9 +48,9 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser - if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException("Uningest File can only be called by Superusers.", this, + // first check if user is a power user + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), uningest)) { + throw new PermissionException(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"), this, Collections.singleton(Permission.EditDataset), uningest); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java index 8cf2d0109d6..0cf9df3d1bb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java @@ -34,8 +34,8 @@ public UpdateDatasetTargetURLCommand( Dataset target, DataverseRequest aRequest) @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { - throw new PermissionException("Update Target URL can only be called by superusers.", + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), target))) { + throw new PermissionException("Update Target URL can only be called by superusers or power users.", this, Collections.singleton(Permission.EditDataset), target); } PidProvider pidProvider = PidUtil.getPidProvider(target.getGlobalId().getProviderId()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java index 6fdbfa59f69..430bf745bf5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; @@ -104,8 +105,8 @@ private void setStringAttribute(String attributeName, Object attributeValue) thr * @throws PermissionException if the user doesn't have permission to modify this attribute. */ private void setBooleanAttribute(CommandContext ctxt, boolean adminOnly) throws CommandException { - if (adminOnly && !getRequest().getUser().isSuperuser()) { - throw new PermissionException("You must be a superuser to change this setting", + if (adminOnly && !(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataverse))) { + throw new PermissionException("You must be a superuser or power user to change this setting", this, Collections.singleton(Permission.EditDataset), dataverse); } @@ -128,8 +129,8 @@ private void setBooleanAttribute(CommandContext ctxt, boolean adminOnly) throws } private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTypesIn) throws CommandException { - if (!getRequest().getUser().isSuperuser()) { - throw new PermissionException("You must be a superuser to change this setting", + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataverse))) { + throw new PermissionException("You must be a superuser or power user to change this setting", this, null, dataverse); } if (!(allowedDatasetTypesIn instanceof String stringValue)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java index 08686e0c682..49e058ba07b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java @@ -7,6 +7,7 @@ import static edu.harvard.iq.dataverse.dataverse.DataverseUtil.validateDataverseMetadataExternally; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; @@ -54,10 +55,11 @@ public UpdateDataverseCommand(Dataverse dataverse, @Override protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandException { + boolean isPowerUser = getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), dataverse); // Perform any optional validation steps, if defined: if (ctxt.systemConfig().isExternalDataverseValidationEnabled()) { // For admins, an override of the external validation step may be enabled: - if (!(getUser().isSuperuser() && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { + if (!(isPowerUser && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { String executable = ctxt.systemConfig().getDataverseValidationExecutable(); boolean result = validateDataverseMetadataExternally(dataverse, executable, getRequest()); @@ -67,7 +69,7 @@ protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandExcep } } } - if (!getUser().isSuperuser() && updatedDataverseDTO != null) { + if (!(isPowerUser) && updatedDataverseDTO != null) { // default if not set if (updatedDataverseDTO.getDatasetFileCountLimit() == null) { updatedDataverseDTO.setDatasetFileCountLimit(dataverse.getDatasetFileCountLimit()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java index 4e6e9cdc00e..980f93b41c3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java @@ -12,7 +12,6 @@ import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.pidproviders.PidUtil; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import java.sql.Timestamp; import java.util.Collections; @@ -37,8 +36,8 @@ public UpdateDvObjectPIDMetadataCommand(Dataset target, DataverseRequest aReques protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { - throw new PermissionException(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.auth.mustBeSuperUser"), + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), target))) { + throw new PermissionException(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.auth.mustBePowerUser"), this, Collections.singleton(Permission.EditDataset), target); } if (!this.target.isReleased()){ diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateHarvestingClientCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateHarvestingClientCommand.java index d8504da6296..55bc0c6dc67 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateHarvestingClientCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateHarvestingClientCommand.java @@ -3,11 +3,16 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.util.BundleUtil; +import java.util.Arrays; +import java.util.Collections; /** * @@ -27,7 +32,7 @@ public UpdateHarvestingClientCommand(DataverseRequest aRequest, HarvestingClient @Override public HarvestingClient execute(CommandContext ctxt) throws CommandException { - // TODO: check that the harvesting client config is attached to a legit + // TODO: check that the harvesting client config is attached to a legit // dataverse; and that we are in fact modifying a config that already // exists. -- L.A. 4.4 return ctxt.em().merge(this.harvestingClient); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java index f8f5d05d972..c081fa0c86d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -34,9 +35,9 @@ public UpdatePublishedDatasetVersionCommand(DataverseRequest aRequest, DatasetVe @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { - // Check if the user is a superuser - if (!getUser().isSuperuser()) { - throw new IllegalCommandException("Only superusers can update published dataset versions", this); + // Check if the user is a superuser or power user + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), datasetVersion.getDataset())) { + throw new IllegalCommandException("Only superusers or power users can update published dataset versions", this); } // Ensure the version is published diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 8273869f6f0..ed8788e1961 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -684,7 +684,7 @@ Whether Harvesting (OAI) service is enabled * the states in an organizations curation process(es) and are intended to help * users/curators track the progress of a dataset through an externally defined * curation process. Only one set of labels are allowed per dataset (defined via - * API by a superuser per collection (UI or API) or per dataset (API only)). A + * API by a poweruser per collection (UI or API) or per dataset (API only)). A * dataset may only have one label at a time and if a label is set, it will be * removed at publication time. This functionality is disabled when this setting * is empty/not set. diff --git a/src/main/java/propertyFiles/BuiltInRoles.properties b/src/main/java/propertyFiles/BuiltInRoles.properties index 50dbb1ba80f..b39078a34d2 100644 --- a/src/main/java/propertyFiles/BuiltInRoles.properties +++ b/src/main/java/propertyFiles/BuiltInRoles.properties @@ -13,4 +13,6 @@ role.dvcontributor.description=A person who can add subdataverses within a datav role.filedownloader.name=File Downloader role.filedownloader.description=A person who can download a published file. role.member.name=Member -role.member.description=A person who can view both unpublished dataverses and datasets. \ No newline at end of file +role.member.description=A person who can view both unpublished dataverses and datasets. +role.poweruser.name=Power User +role.poweruser.description=A person who can take any action on a dvobject that a superuser could. \ No newline at end of file diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 66dd1d1d659..7d041f3a826 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1018,13 +1018,13 @@ dataverse.storage.quota.allocation=Total quota allocation for this collection: { dataverse.storage.quota.notdefined=No quota defined for this collection dataverse.storage.quota.updated=Storage quota successfully set for the collection dataverse.storage.quota.deleted=Storage quota successfully disabled for the collection -dataverse.storage.quota.superusersonly=Only superusers can change storage quotas. +dataverse.storage.quota.powerusersonly=Only superusers and those with the scopedPowerUser permission on an item can change storage quotas. dataverse.storage.use=Total recorded size of the files stored in this collection (user-uploaded files plus the versions in the archival tab-delimited format and/or auxiliary files, when applicable): {0} bytes dataset.storage.quota.allocation=Total quota allocation for this dataset: {0} bytes dataset.storage.quota.notdefined=No quota defined for this dataset dataset.storage.quota.updated=Storage quota successfully set for the dataset dataset.storage.quota.deleted=Storage quota successfully disabled for the dataset -dataset.storage.quota.superusersonly=Only superusers can change storage quotas. +dataset.storage.quota.powerusersonly=Only superusers and those with the scopedPowerUser permission on an item can change storage quotas. dataset.storage.use=Total recorded size of the files stored in this dataset (user-uploaded files plus the versions in the archival tab-delimited format and/or auxiliary files, when applicable): {0} bytes dataverse.datasize.ioerror=Fatal IO error while trying to determine the total size of the files stored in the dataverse. Please report this error to the Dataverse administrator. dataverse.inherited=(inherited from enclosing Dataverse) @@ -2618,6 +2618,7 @@ permission.downloadFile=Download a file permission.viewUnpublishedDataset=View an unpublished dataset and its files permission.viewUnpublishedDataverse=View an unpublished dataverse permission.addDatasetDataverse=Add a dataset to a dataverse +permission.scopedPowerUser=Allow a user to take any action on a dvobject that a superuser could #DataverseUserPage.java userPage.informationUpdated=Your account information has been successfully updated. @@ -2778,7 +2779,7 @@ permission.permissionsMissing=Permissions {0} missing. permission.CannotAssigntDefaultPermissions=Cannot assign default permissions. permission.default.contributor.role.none.decription=A person who has no permissions on a newly created dataset. Not recommended for dataverses with human contributors. permission.default.contributor.role.none.name=None -permission.role.must.be.created.by.superuser=Roles can only be created or edited by superusers. +permission.role.must.be.created.by.poweruser=Roles can only be created or edited by superusers or those with scopedPowerUser permission for this collection. permission.role.not.created.alias.already.exists=Role with this alias already exists. #ManageFilePermissionsPage.java @@ -2828,10 +2829,12 @@ permission.cannotSaveRole=Cannot save role {0} pid.allowedCharacters=^[A-Za-z0-9._/:\\-]* #General Command Exception -command.exception.only.superusers={1} can only be called by superusers. +command.exception.only.powerusers={1} can only be called by superusers or those with scopedPowerUser permission for this item. command.exception.user.deactivated={0} failed: User account has been deactivated. command.exception.user.deleted={0} failed: User account has been deleted. command.exception.user.ratelimited={0} failed: Rate limited due to too many requests. +#General API +api.auth.mustBePowerUser=Forbidden. You must be a superuser or have the scopedPowerUser permission for this item. #Admin-API admin.api.auth.mustBeSuperUser=Forbidden. You must be a superuser. @@ -2858,7 +2861,7 @@ files.api.fileNotFound=File could not be found. #Datasets.java datasets.api.updatePIDMetadata.failure.dataset.must.be.released=Modify Registration Metadata must be run on a published dataset. -datasets.api.updatePIDMetadata.auth.mustBeSuperUser=Forbidden. You must be a superuser. +datasets.api.updatePIDMetadata.auth.mustBePowerUser=Forbidden. You must be a superuser or have the scopedPowerUser permission for this item. datasets.api.updatePIDMetadata.success.for.single.dataset=Dataset {0} PID Metadata updated successfully. datasets.api.updatePIDMetadata.success.for.update.all=All Dataset PID Metadata update completed. See log for any issues. datasets.api.moveDataset.error.targetDataverseNotFound=Target dataverse not found. @@ -2925,7 +2928,7 @@ dataverses.api.move.dataverse.error.dataverseLink=Dataverse is linked to target dataverses.api.move.dataverse.error.datasetLink=Dataset is linked to target dataverse or one of its parents. dataverses.api.move.dataverse.error.forceMove=Please use the API and see "Move a Dataverse Collection" with the parameter ?forceMove=true to complete the move. This will remove anything from the dataverse that is not compatible with the target dataverse. dataverses.api.create.dataset.error.mustIncludeVersion=Please provide initial version in the dataset json -dataverses.api.create.dataset.error.superuserFiles=Only a superuser may add files via this api +dataverses.api.create.dataset.error.powerUserFiles=Only a superuser or those with the scopedPowerUser permission may add files via this api dataverses.api.create.dataset.error.mustIncludeAuthorName=Please provide author name in the dataset json dataverses.api.validate.json.succeeded=The Dataset JSON provided is valid for this Dataverse Collection. dataverses.api.validate.json.failed=The Dataset JSON provided failed validation with the following error: @@ -2998,6 +3001,7 @@ permission.LinkDataset.label=LinkDataset permission.DeleteDataverse.label=DeleteDataverse permission.DeleteDatasetDraft.label=DeleteDatasetDraft permission.ManageFilePermissions.label=ManageFilePermissions +permission.ScopedPowerUser.label=Scoped Power User permission.AddDataverse.desc=Add a dataverse within another dataverse permission.DeleteDatasetDraft.desc=Delete a dataset draft @@ -3015,6 +3019,7 @@ permission.DownloadFile.desc=Download a file permission.ViewUnpublishedDataset.desc=View an unpublished dataset and its files permission.ViewUnpublishedDataverse.desc=View an unpublished dataverse permission.AddDataset.desc=Add a dataset to a dataverse +permission.ScopedPowerUser.desc=Allow a user to take any action on a dvobject that a superuser could packageDownload.title=Package File Download packageDownload.instructions=Use the Download URL in a Wget command or a download manager to download this package file. Download via web browser is not recommended. User Guide - Downloading a Dataverse Package via URL diff --git a/src/main/resources/db/migration/V6.10.1.2__10588-add-power-user-role.sql b/src/main/resources/db/migration/V6.10.1.2__10588-add-power-user-role.sql new file mode 100644 index 00000000000..01af4985d03 --- /dev/null +++ b/src/main/resources/db/migration/V6.10.1.2__10588-add-power-user-role.sql @@ -0,0 +1,4 @@ +-- Add the Scoped Power User role +INSERT INTO dataverserole (alias, name, description, permissionbits, owner_id) +VALUES ('powerUser', 'Power User', 'A person who can take any action on a dvobject that a superuser could.', 65536, NULL) +ON CONFLICT (alias) DO NOTHING; diff --git a/src/main/webapp/dashboard.xhtml b/src/main/webapp/dashboard.xhtml index 577d8c67f63..38464d10765 100644 --- a/src/main/webapp/dashboard.xhtml +++ b/src/main/webapp/dashboard.xhtml @@ -25,7 +25,7 @@
-
+

#{bundle['dashboard.card.harvestingclients.header']}

@@ -58,7 +58,7 @@
-
+

#{bundle['dashboard.card.harvestingserver.header']}

@@ -103,7 +103,7 @@
-
+

#{bundle['dashboard.card.users']}

diff --git a/src/main/webapp/dataset-versions.xhtml b/src/main/webapp/dataset-versions.xhtml index df5a39c09b7..658aeee7c0a 100644 --- a/src/main/webapp/dataset-versions.xhtml +++ b/src/main/webapp/dataset-versions.xhtml @@ -164,22 +164,22 @@ - + - + - + - + - - - - + + + + diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 24f2bb64be6..da6c3000f95 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -2083,7 +2083,7 @@ - + diff --git a/src/main/webapp/dataverse.xhtml b/src/main/webapp/dataverse.xhtml index 5a42b4b1f8e..e5bfe8b7c47 100644 --- a/src/main/webapp/dataverse.xhtml +++ b/src/main/webapp/dataverse.xhtml @@ -149,7 +149,7 @@
-
+
#{bundle.storage}
-
+
#{bundle.curationLabels}
-
+
#{bundle.pidProviderOption} -
+
#{bundle['dataverse.locallyfair.label']}
  • -
  • +
  • diff --git a/src/main/webapp/file-edit-button-fragment.xhtml b/src/main/webapp/file-edit-button-fragment.xhtml index 8b07aa50929..aaaeb6f1962 100644 --- a/src/main/webapp/file-edit-button-fragment.xhtml +++ b/src/main/webapp/file-edit-button-fragment.xhtml @@ -79,14 +79,14 @@ - +
  • - +
  • diff --git a/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java new file mode 100644 index 00000000000..c12cfafce0a --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java @@ -0,0 +1,279 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PermissionServiceBeanTest { + + @InjectMocks + private PermissionServiceBean permissionService; + + @Mock + private GroupServiceBean groupService; + + @Mock + private DataverseRoleServiceBean roleService; + + @Mock + private DvObjectServiceBean dvObjectServiceBean; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testIsPowerUserOn_Superuser() { + AuthenticatedUser superuser = mock(AuthenticatedUser.class); + when(superuser.isSuperuser()).thenReturn(true); + Dataverse dv = new Dataverse(); + + assertTrue(permissionService.isPowerUserOn(superuser, dv)); + } + + @Test + public void testIsPowerUserOn_NullUser() { + Dataverse dv = new Dataverse(); + assertFalse(permissionService.isPowerUserOn(null, dv)); + } + + @Test + public void testIsPowerUserOn_NullDvObject() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + assertFalse(permissionService.isPowerUserOn(user, null)); + } + + @Test + public void testIsPowerUserOn_WithAssignment() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + + Dataverse dv = new Dataverse(); + dv.setId(1L); + + when(groupService.groupsFor(eq(user), eq(dv))).thenReturn(Collections.emptySet()); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, dv, null); + + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + assertTrue(permissionService.isPowerUserOn(user, dv)); + } + + @Test + public void testIsPowerUserOn_WithParentAssignment() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + + Dataverse parent = new Dataverse(); + parent.setId(1L); + parent.setPermissionRoot(true); + + Dataverse child = new Dataverse(); + child.setId(2L); + child.setOwner(parent); + + when(groupService.groupsFor(eq(user), any())).thenReturn(Collections.emptySet()); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, parent, null); + + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + assertTrue(permissionService.isPowerUserOn(user, child)); + } + + @Test + public void testIsPowerUserOn_BlockedNormalAdmin() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + Dataverse dv = new Dataverse(); + dv.setId(1L); + + when(groupService.groupsFor(eq(user), eq(dv))).thenReturn(Collections.emptySet()); + + DataverseRole adminRole = new DataverseRole(); + adminRole.addPermission(Permission.EditDataverse); + + RoleAssignment assignment = new RoleAssignment(adminRole, user, dv, null); + + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + assertFalse(permissionService.isPowerUserOn(user, dv)); + } + + @Test + public void testHasPermissionsFor_PowerUser() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + Dataverse dv = new Dataverse(); + dv.setId(1L); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, dv, null); + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + assertTrue(permissionService.hasPermissionsFor(user, dv, EnumSet.of(Permission.AddDataverse))); + } + + @Test + public void testPermissionsFor_PowerUser() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + Dataverse dv = new Dataverse(); + dv.setId(1L); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, dv, null); + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + Set perms = permissionService.permissionsFor(user, dv); + assertEquals(EnumSet.allOf(Permission.class), perms); + } + + @Test + public void testWhichChildrenHasPermissionsFor_PowerUser() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + + Dataverse parent = new Dataverse(); + parent.setId(1L); + + Dataverse child1 = new Dataverse(); + child1.setId(2L); + Dataverse child2 = new Dataverse(); + child2.setId(3L); + + List children = Arrays.asList(child1, child2); + when(dvObjectServiceBean.findByOwnerId(1L)).thenReturn(children); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, parent, null); + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + DataverseRequest req = mock(DataverseRequest.class); + when(req.getUser()).thenReturn(user); + + List result = permissionService.whichChildrenHasPermissionsFor(req, parent, EnumSet.of(Permission.AddDataverse)); + assertEquals(2, result.size()); + assertTrue(result.contains(child1)); + assertTrue(result.contains(child2)); + } + + @Test + public void testWhichChildrenHasPermissionsFor_PowerUserOnSingleChild() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + + Dataverse parent = new Dataverse(); + parent.setId(1L); + + Dataverse child1 = new Dataverse(); + child1.setId(2L); + child1.setOwner(parent); + Dataverse child2 = new Dataverse(); + child2.setId(3L); + child2.setOwner(parent); + + List children = Arrays.asList(child1, child2); + when(dvObjectServiceBean.findByOwnerId(1L)).thenReturn(children); + + // Power User assignment only on child1 + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, child1, null); + + // Group service returns empty for both + when(groupService.groupsFor(any(DataverseRequest.class))).thenReturn(Collections.emptySet()); + when(groupService.groupsFor(eq(user), any())).thenReturn(Collections.emptySet()); + + // roleService.directRoleAssignments should return the assignment for child1 when called with children + // We need to capture the set of objects passed to directRoleAssignments or use a matcher + when(roleService.directRoleAssignments(anySet(), anyCollection())).thenAnswer(invocation -> { + Collection dvos = invocation.getArgument(1); + if (dvos != null && dvos.contains(child1)) { + return new ArrayList<>(Collections.singletonList(assignment)); + } + return new ArrayList<>(); + }); + + DataverseRequest req = mock(DataverseRequest.class); + when(req.getUser()).thenReturn(user); + + List result = permissionService.whichChildrenHasPermissionsFor(req, parent, EnumSet.of(Permission.AddDataverse)); + + assertEquals(1, result.size()); + assertTrue(result.contains(child1)); + assertFalse(result.contains(child2)); + } + + @Test + public void testHasLocallyFAIRAccess_PowerUser() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + when(user.isAuthenticated()).thenReturn(true); + when(user.getIdentifier()).thenReturn("@user"); + + Dataverse dv = new Dataverse(); + dv.setId(1L); + dv.setLocallyFAIRRoleAssigneeIdentifiers(Collections.singleton("@some_group")); + + DataverseRole powerUserRole = new DataverseRole(); + powerUserRole.addPermission(Permission.ScopedPowerUser); + RoleAssignment assignment = new RoleAssignment(powerUserRole, user, dv, null); + when(roleService.directRoleAssignments(anySet(), anySet())).thenReturn(new ArrayList<>(Collections.singletonList(assignment))); + + DataverseRequest req = mock(DataverseRequest.class); + when(req.getUser()).thenReturn(user); + + assertTrue(permissionService.hasLocallyFAIRAccess(req, dv)); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/PermissionsWrapperTest.java b/src/test/java/edu/harvard/iq/dataverse/PermissionsWrapperTest.java index 751a90fb447..443cf94617a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/PermissionsWrapperTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/PermissionsWrapperTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -24,6 +25,7 @@ public void setUp() { this.permissionWrapper = new PermissionsWrapper(); this.permissionWrapper.permissionService = mock(PermissionServiceBean.class); this.permissionWrapper.dvRequestService = mock(DataverseRequestServiceBean.class); + this.permissionWrapper.session = mock(DataverseSession.class); } @AfterEach @@ -31,6 +33,30 @@ public void tearDown() { this.permissionWrapper = null; } + @Test + public void testIsPowerUserOn() { + Dataverse dv = new Dataverse(); + dv.setId(1L); + AuthenticatedUser user = mock(AuthenticatedUser.class); + + Mockito.when(this.permissionWrapper.session.getUser()).thenReturn(user); + Mockito.when(this.permissionWrapper.permissionService.isPowerUserOn(user, dv)).thenReturn(true); + + assertTrue(this.permissionWrapper.isPowerUserOn(dv)); + } + + @Test + public void testIsPowerUserOnSomeDvObject() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + Mockito.when(user.isSuperuser()).thenReturn(false); + Mockito.when(user.isAuthenticated()).thenReturn(true); + + Mockito.when(this.permissionWrapper.session.getUser()).thenReturn(user); + Mockito.when(this.permissionWrapper.permissionService.isPowerUserOnSomeDvObject(user)).thenReturn(true); + + assertTrue(this.permissionWrapper.isPowerUserOnSomeDvObject()); + } + @Test public void testCanManageDatasetPermissionsWithUndefinedDataverse() { Dataverse dataverse = null; diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java index af6c5a72623..5ff1dc2a9d7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevelServiceBean; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.authorization.DataverseRole; @@ -214,6 +215,15 @@ public DataverseFacetServiceBean facets() { public DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels() { return dfils; } + @Override + public PermissionServiceBean permissions() { + return new PermissionServiceBean() { + @Override + public boolean isPowerUserOn(AuthenticatedUser user, edu.harvard.iq.dataverse.DvObject dvo) { + return user != null && user.isSuperuser(); + } + }; + } } ); } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommandTest.java index 3d947879e56..b68162182b0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateRoleCommandTest.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; @@ -51,6 +52,16 @@ public EntityManager em() { return new LocalTestEntityManager(); } + + @Override + public PermissionServiceBean permissions() { + return new PermissionServiceBean() { + @Override + public boolean isPowerUserOn(AuthenticatedUser user, edu.harvard.iq.dataverse.DvObject dvo) { + return user != null && user.isSuperuser(); + } + }; + } }); @BeforeEach diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommandTest.java index 3c3188da830..3ecf9456f02 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDataverseCommandTest.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseLinkingServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.Guestbook; import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.Template; @@ -273,6 +274,15 @@ public DataverseLinkingServiceBean dvLinking() { }; } + @Override + public PermissionServiceBean permissions() { + return new PermissionServiceBean() { + @Override + public boolean isPowerUserOn(AuthenticatedUser user, edu.harvard.iq.dataverse.DvObject dvo) { + return user != null && user.isSuperuser(); + } + }; + } }); } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommandTest.java index 2f607aa7a0e..ac376d34a61 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/SubmitDatasetForReviewCommandTest.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.PermissionServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; @@ -129,9 +130,18 @@ public List getUsersWithPermissionOn(Permission permission, D // We only need permissions for notifications, which we are testing in InReviewWorkflowIT. return Collections.emptyList(); } + @Override + public boolean isPowerUserOn(AuthenticatedUser user, edu.harvard.iq.dataverse.DvObject dvo) { + return user != null && user.isSuperuser(); + } }; } + @Override + public GroupServiceBean groups() { + return new GroupServiceBean(); + } + } ); }