From 226554263c5884775e7bb034ae341519ddd9304b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 12 Jun 2026 14:43:11 -0400 Subject: [PATCH 01/11] new per/role for #10588 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary • Implemented the first stage of the Scoped Power Admin feature. • Added a new ScopedPowerAdmin permission and a PowerAdmin role alias. • Created isPowerUser functionality and integrated it into the setStorageDriver API call. Changes • Modified Permission.java to include the ScopedPowerAdmin permission. • Updated DataverseRole.java with a new POWER_ADMIN role constant. • Added isPowerUser(AuthenticatedUser user, DvObject dvo) to PermissionServiceBean.java to check for superuser status or the new permission. • Updated the setStorageDriver method in Dataverses.java to use permissionSvc.isPowerUser instead of a hardcoded isSuperuser check. • Added localized strings for the new permission and role in Bundle.properties and BuiltInRoles.properties. --- .../harvard/iq/dataverse/PermissionServiceBean.java | 13 +++++++++++++ .../edu/harvard/iq/dataverse/api/Dataverses.java | 4 ++-- .../iq/dataverse/authorization/DataverseRole.java | 1 + .../iq/dataverse/authorization/Permission.java | 3 ++- src/main/java/propertyFiles/BuiltInRoles.properties | 4 +++- src/main/java/propertyFiles/Bundle.properties | 3 +++ 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 402a1b06e3c..aa8e1577269 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -494,6 +494,19 @@ public Set permissionsFor(RoleAssignee ra, DvObject dvo) { } return permissions; } + + public boolean isPowerUser(AuthenticatedUser user, DvObject dvo) { + if (user == null) { + return false; + } + if (user.isSuperuser()) { + return true; + } + if (dvo == null) { + return false; + } + return hasPermissionsFor(user, dvo, EnumSet.of(Permission.ScopedPowerAdmin)); + } private void addGroupPermissionsFor(Set ras, DvObject dvo, Set permissions) { for (RoleAssignment asmnt : assignmentsFor(ras, dvo)) { 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 0c0aa59057c..5f14e297277 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -2212,8 +2212,8 @@ public Response setStorageDriver(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUser(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or Power Admins only."); } } catch (WrappedResponse wr) { return wr.getResponse(); 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..b6674b4e4d3 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_ADMIN = "powerAdmin"; 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..d61eaafd591 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), + ScopedPowerAdmin(BundleUtil.getStringFromBundle("permission.scopedPowerAdmin"), true, DvObject.class); // FUTURE: diff --git a/src/main/java/propertyFiles/BuiltInRoles.properties b/src/main/java/propertyFiles/BuiltInRoles.properties index 50dbb1ba80f..b990611b85e 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.poweradmin.name=Power Admin +role.poweradmin.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 9a8c97fe429..8ee646488c1 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2607,6 +2607,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.scopedPowerAdmin=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. @@ -2983,6 +2984,7 @@ permission.LinkDataset.label=LinkDataset permission.DeleteDataverse.label=DeleteDataverse permission.DeleteDatasetDraft.label=DeleteDatasetDraft permission.ManageFilePermissions.label=ManageFilePermissions +permission.ScopedPowerAdmin.label=Scoped Power Admin permission.AddDataverse.desc=Add a dataverse within another dataverse permission.DeleteDatasetDraft.desc=Delete a dataset draft @@ -3000,6 +3002,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.ScopedPowerAdmin.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 From c3e899c39037cb9f6dce96c156c83b90fe9e7bfa Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 17 Jul 2026 15:51:55 -0400 Subject: [PATCH 02/11] accept isPowerUser changes w/o full review --- .../edu/harvard/iq/dataverse/DatasetPage.java | 13 +- .../harvard/iq/dataverse/DataversePage.java | 4 +- .../iq/dataverse/EditDatafilesPage.java | 4 +- .../iq/dataverse/HarvestingClientsPage.java | 2 - .../iq/dataverse/HarvestingSetsPage.java | 2 - .../iq/dataverse/PermissionsWrapper.java | 10 +- .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../edu/harvard/iq/dataverse/api/Access.java | 4 +- .../edu/harvard/iq/dataverse/api/Admin.java | 48 ++-- .../harvard/iq/dataverse/api/Datasets.java | 229 +++++++++--------- .../harvard/iq/dataverse/api/Dataverses.java | 32 +-- .../edu/harvard/iq/dataverse/api/Files.java | 13 +- .../iq/dataverse/api/HarvestingClients.java | 114 ++++----- .../edu/harvard/iq/dataverse/api/Pids.java | 3 +- .../datasetutility/AddReplaceFileHelper.java | 4 +- .../command/impl/AbstractDatasetCommand.java | 6 +- .../command/impl/CreateDataverseCommand.java | 2 +- .../command/impl/CreateRoleCommand.java | 4 +- .../CuratePublishedDatasetVersionCommand.java | 5 +- .../impl/DeleteCollectionQuotaCommand.java | 4 +- .../command/impl/DeleteDataFileCommand.java | 4 +- .../impl/DeleteDatasetQuotaCommand.java | 2 +- ...eleteDataverseLinkingDataverseCommand.java | 4 +- .../impl/DeleteHarvestingClientCommand.java | 57 ----- .../engine/command/impl/DeletePidCommand.java | 4 +- .../command/impl/DestroyDatasetCommand.java | 4 +- .../command/impl/LinkDataverseCommand.java | 4 +- .../impl/ListDataverseContentCommand.java | 3 +- .../command/impl/MoveDatasetCommand.java | 10 +- .../command/impl/MoveDataverseCommand.java | 4 +- .../command/impl/PublishDatasetCommand.java | 4 +- .../command/impl/PublishDataverseCommand.java | 2 +- .../command/impl/ReservePidCommand.java | 4 +- .../impl/SetCollectionQuotaCommand.java | 4 +- .../impl/SubmitDatasetForReviewCommand.java | 2 +- .../command/impl/UningestFileCommand.java | 5 +- .../impl/UpdateDatasetTargetURLCommand.java | 4 +- .../impl/UpdateDataverseAttributeCommand.java | 8 +- .../command/impl/UpdateDataverseCommand.java | 4 +- .../UpdateDvObjectPIDMetadataCommand.java | 4 +- .../impl/UpdateHarvestingClientCommand.java | 9 + .../UpdatePublishedDatasetVersionCommand.java | 7 +- src/main/java/propertyFiles/Bundle.properties | 14 +- src/main/webapp/dataset-versions.xhtml | 18 +- 44 files changed, 307 insertions(+), 383 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteHarvestingClientCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index b60633fca5f..1478440f0e2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -1512,16 +1512,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.isPowerUser((AuthenticatedUser) session.getUser(), dataset); } /** * Check Dataset related permissions @@ -4161,7 +4158,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 @@ -6611,7 +6608,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()); @@ -6806,7 +6803,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 d9cafbf421a..e597577fb01 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -208,7 +208,7 @@ public void setLinkMode(LinkMode linkMode) { public boolean showLinkingPopup() { String testquery = ""; - if (session.getUser() == null) { + if (session.getUser() == null || !session.getUser().isAuthenticated()) { return false; } if (dataverse == null) { @@ -218,7 +218,7 @@ public boolean showLinkingPopup() { testquery = query; } - return (session.getUser().isSuperuser() && (dataverse.getOwner() != null || !testquery.isEmpty())); + return (permissionService.isPowerUser((AuthenticatedUser) session.getUser(), dataverse) && (dataverse.getOwner() != null || !testquery.isEmpty())); } public void setupLinkingPopup (String popupSetting){ diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 46d17d05363..0f66c2914bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -558,7 +558,7 @@ public boolean isQuotaExceeded() { return systemConfig.isStorageQuotasEnforced() && uploadSessionQuota != null && uploadSessionQuota.getRemainingQuotaInBytes() == 0; } public boolean isFileUploadCountExceeded() { - boolean ignoreLimit = this.session.getUser().isSuperuser(); + boolean ignoreLimit = permissionService.isPowerUser((AuthenticatedUser) session.getUser(), dataset); return !ignoreLimit && !isFileReplaceOperation() && fileUploadsAvailable != null && fileUploadsAvailable == 0; } @@ -1122,7 +1122,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.isPowerUser((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..d64380e8d58 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java @@ -110,8 +110,6 @@ public List completeSelectedDataverse(String query) { public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); - } else if (!isSuperUser()) { - return navigationWrapper.notAuthorized(); } if (dataverseId != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java index 0b66b652e0c..e2a7a58bf79 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java @@ -123,8 +123,6 @@ public void setOaiServerStatusRadio(int oaiServerStatusRadio) { public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); - } else if (!isSuperUser()) { - return navigationWrapper.notAuthorized(); } configuredHarvestingSets = oaiSetService.findAll(); diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 4821d0c7289..d2a24fb4f99 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -7,6 +7,7 @@ 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.User; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -255,7 +256,7 @@ public boolean canIssueDeleteDatasetCommand(DvObject dvo){ // PUBLISH DATASET public boolean canIssuePublishDatasetCommand(DvObject dvo){ User u = session.getUser(); - if (u != null && u.isSuperuser()) { + if (u instanceof AuthenticatedUser && permissionService.isPowerUser((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 @@ -316,7 +317,12 @@ public boolean authUsersCanCreateDatasetsInDataverse(Dataverse dataverse) { public boolean authUsersCanCreateDataversesInDataverse(Dataverse dataverse) { return authenticatedUsersCanIssueCommand(dataverse, CreateDataverseCommand.class); } - + + public boolean isPowerUserOn(DvObject dvo) { + User u = session.getUser(); + return (u instanceof AuthenticatedUser && permissionService.isPowerUser((AuthenticatedUser) u, dvo)); + } + // todo: move any calls to this to call NavigationWrapper @Inject NavigationWrapper navigationWrapper; 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 94785ce3976..484d7e6fc85 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -750,7 +750,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.isPowerUser(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 c3c74f49019..cf52df7a5c5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -1764,7 +1764,7 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa List args = Arrays.asList(wr.getLocalizedMessage()); return error(UNAUTHORIZED, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", args)); } - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { + if (!(permissionService.isPowerUser(dataverseRequest.getAuthenticatedUser(), dataFile) || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } @@ -1963,7 +1963,7 @@ public Response rejectFileAccess(@Context ContainerRequestContext crc, @PathPara dataverseRequest = createDataverseRequest(getRequestUser(crc)); - if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() || permissionService.requestOn(dataverseRequest, dataFile).has(Permission.ManageFilePermissions))) { + if (!(permissionService.isPowerUser(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 919fa7f67f9..9a7c6db5868 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -1597,16 +1597,15 @@ public Response isOrcidEnabled() { 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.isPowerUser((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 " ); @@ -1740,9 +1739,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.isPowerUser(user, collection)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); } if (!systemConfig.isFilePIDsEnabledForCollection(collection)) { @@ -1772,7 +1776,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) { @@ -1929,8 +1933,9 @@ public Response computeDataFileHashValue(@Context ContainerRequestContext crc, @ try { User u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(Status.UNAUTHORIZED, "must be superuser"); + DataFile fileToUpdate = findDataFileOrDie(fileId); + if (!permissionSvc.isPowerUser((AuthenticatedUser) u, fileToUpdate)) { + return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse e1) { return error(Status.UNAUTHORIZED, "api key required"); @@ -1991,8 +1996,9 @@ public Response validateDataFileHashValue(@Context ContainerRequestContext crc, try { User u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(Status.UNAUTHORIZED, "must be superuser"); + DataFile fileToValidate = findDataFileOrDie(fileId); + if (!permissionSvc.isPowerUser((AuthenticatedUser) u, fileToValidate)) { + return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse e1) { return error(Status.UNAUTHORIZED, "api key required"); @@ -2235,8 +2241,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.isPowerUser(user, owner)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2266,8 +2272,8 @@ public Response getCurationLabelSet(@Context ContainerRequestContext crc, @PathP } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUser(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2288,8 +2294,8 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, @PathP } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUser(user, dataverse)) { + return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2319,8 +2325,8 @@ public Response resetCurationLabelSet(@Context ContainerRequestContext crc, @Pat } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUser(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 72182b3d42b..df0b84ecea0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -328,8 +328,8 @@ public Response deleteDataset(@Context ContainerRequestContext crc, @PathParam(" 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.isPowerUser((AuthenticatedUser) u, doomed))) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"))); } destroy = true; } else { @@ -367,8 +367,8 @@ public Response destroyDataset(@Context ContainerRequestContext crc, @PathParam( // first check if dataset is released, and if so, if user is a superuser 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.isPowerUser((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 @@ -1238,6 +1238,8 @@ public Response publishDataset(@Context ContainerRequestContext crc, @PathParam( } boolean updateCurrent=false; AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); + Dataset ds = findDatasetOrDie(id); + type = type.toLowerCase(); boolean isMinor=false; switch (type) { @@ -1248,17 +1250,17 @@ public Response publishDataset(@Context ContainerRequestContext crc, @PathParam( isMinor = false; break; case "updatecurrent": - if (user.isSuperuser()) { + if (permissionSvc.isPowerUser(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) { @@ -1370,11 +1372,10 @@ public Response publishDataset(@Context ContainerRequestContext crc, @PathParam( public Response publishMigratedDataset(@Context ContainerRequestContext crc, String jsonldBody, @PathParam("id") String id, @DefaultValue("false") @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.isPowerUser(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()); @@ -1519,8 +1520,8 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar * 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.isPowerUser(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 @@ -1597,9 +1598,10 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar if (datasetFiles.containsAll(filesToEmbargo)) { JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); for (DataFile datafile : filesToEmbargo) { // superuser can overrule an existing embargo, even on released files - if (datafile.isReleased() && !authenticatedUser.isSuperuser()) { + if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -1634,7 +1636,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar } } //If superuser, report changes to any released files - if (authenticatedUser.isSuperuser()) { + if (permissionSvc.isPowerUser(authenticatedUser, dataset)) { String releasedFiles = filesToEmbargo.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); if (!releasedFiles.isBlank()) { @@ -1673,11 +1675,10 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @PathPar 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.isPowerUser(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 ScopedPowerAdmin 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 @@ -1718,9 +1719,10 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @PathPar if (datasetFiles.containsAll(embargoFilesToUnset)) { JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUser(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())) { + if (datafile.getEmbargo()==null || ((datafile.isReleased() && datafile.getEmbargo() != null) && !isPowerUser)) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -1791,8 +1793,8 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @PathP // 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.isPowerUser(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 ScopedPowerAdmin 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 @@ -1889,9 +1891,10 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @PathP if (datasetFiles.containsAll(filesToRetention)) { JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); for (DataFile datafile : filesToRetention) { // superuser can overrule an existing retention, even on released files - if (datafile.isReleased() && !authenticatedUser.isSuperuser()) { + if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; } @@ -1926,7 +1929,7 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @PathP } } //If superuser, report changes to any released files - if (authenticatedUser.isSuperuser()) { + if (isPowerUser) { String releasedFiles = filesToRetention.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); if (!releasedFiles.isBlank()) { @@ -1966,8 +1969,8 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @PathP // 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.isPowerUser(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 @@ -2019,9 +2022,10 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @PathP if (datasetFiles.containsAll(retentionFilesToUnset)) { JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); boolean badFiles = false; + boolean isPowerUser = permissionSvc.isPowerUser(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; } @@ -2078,9 +2082,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 { @@ -2089,6 +2090,10 @@ public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } + if (!permissionSvc.isPowerUser(authenticatedUser, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setDatasetFileCountLimit(datasetFileCountLimit); datasetService.merge(dataset); return ok("ok"); @@ -2106,9 +2111,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 { @@ -2117,6 +2119,10 @@ public Response deleteDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } + if (!permissionSvc.isPowerUser(authenticatedUser, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + dataset.setDatasetFileCountLimit(null); datasetService.merge(dataset); return ok("ok"); @@ -2509,12 +2515,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.isPowerUser(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."); @@ -2788,7 +2794,7 @@ public Response getMPUploadUrls(@Context ContainerRequestContext crc, @PathParam 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.isPowerUser((AuthenticatedUser) user, dataset)) { Integer effectiveDatasetFileCountLimit = dataset.getEffectiveDatasetFileCountLimit(); boolean hasFileCountLimit = dataset.isDatasetFileCountLimitSet(effectiveDatasetFileCountLimit); if (hasFileCountLimit) { @@ -3382,10 +3388,10 @@ public Response deleteLocks(@Context ContainerRequestContext crc, @PathParam("id 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.isPowerUser(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } if (lockType == null) { Set locks = new HashSet<>(); @@ -3432,10 +3438,10 @@ public Response lockDataset(@Context ContainerRequestContext crc, @PathParam("id 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.isPowerUser(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); @@ -3734,25 +3740,24 @@ public Response setFileStore(@Context ContainerRequestContext crc, @PathParam("i 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.isPowerUser(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)) { @@ -3771,25 +3776,24 @@ public Response setFileStore(@Context ContainerRequestContext crc, @PathParam("i public Response resetFileStore(@Context ContainerRequestContext crc, @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.isPowerUser(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); @@ -3801,23 +3805,22 @@ public Response resetFileStore(@Context ContainerRequestContext crc, @PathParam( public Response getCurationLabelSet(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - try { - AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); - } - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - Dataset dataset; - try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return ex.getResponse(); } + try { + AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); + if (!permissionSvc.isPowerUser(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + return response(req -> ok(dataset.getEffectiveCurationLabelSetName()), getRequestUser(crc)); } @@ -3830,24 +3833,23 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, @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.isPowerUser(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); @@ -3871,25 +3873,24 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, public Response resetCurationLabelSet(@Context ContainerRequestContext crc, @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.isPowerUser(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); @@ -5063,11 +5064,12 @@ public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); - if (!au.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + Dataset ds = findDatasetOrDie(datasetId); + if (!permissionSvc.isPowerUser(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.getArchivalCopyLocation() == null) { @@ -5095,9 +5097,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.isPowerUser(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). @@ -5109,7 +5111,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) { @@ -5151,12 +5153,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.isPowerUser(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"); @@ -5559,24 +5561,23 @@ public Response setguestbookEntryAtRequest(@Context ContainerRequestContext crc, 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.isPowerUser(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."); @@ -5593,25 +5594,24 @@ public Response setguestbookEntryAtRequest(@Context ContainerRequestContext crc, public Response resetGuestbookEntryAtRequest(@Context ContainerRequestContext crc, @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.isPowerUser(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()); @@ -5656,25 +5656,21 @@ public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext cr @AuthRequired @Path("{identifier}/pidReconcile") public Response reconcilePid(@Context ContainerRequestContext crc, @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.isPowerUser(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()); @@ -5715,24 +5711,23 @@ public Response getPidGenerator(@Context ContainerRequestContext crc, @PathParam public Response setPidGenerator(@Context ContainerRequestContext crc, @PathParam("identifier") String datasetId, 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.isPowerUser(user, dataset)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } if (PidUtil.getManagedProviderIds().contains(generatorId)) { dataset.setPidGeneratorId(generatorId); datasetService.merge(dataset); @@ -5749,25 +5744,24 @@ public Response setPidGenerator(@Context ContainerRequestContext crc, @PathParam public Response resetPidGenerator(@Context ContainerRequestContext crc, @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.isPowerUser(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()); @@ -6180,12 +6174,12 @@ public Response addVersionNote(@Context ContainerRequestContext crc, @PathParam( 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.isPowerUser(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()); @@ -6212,8 +6206,9 @@ public Response deleteVersionNote(@Context ContainerRequestContext crc, @PathPar } 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.isPowerUser(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 5f14e297277..0482687aa4e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -390,8 +390,8 @@ public Response createDataset(@Context ContainerRequestContext crc, String jsonB 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.isPowerUser((AuthenticatedUser) u, owner))){ + return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.powerUserFiles")); } //Throw BadRequestException if metadataLanguage isn't compatible with setting @@ -499,10 +499,10 @@ public Response createDatasetFromJsonLd(@Context ContainerRequestContext crc, St public Response importDataset(@Context ContainerRequestContext crc, String jsonBody, @PathParam("identifier") String parentIdtf, @QueryParam("pid") String pidParam, @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.isPowerUser((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } Dataset ds = parseDataset(jsonBody); ds.setOwner(owner); @@ -580,10 +580,10 @@ public Response importDataset(@Context ContainerRequestContext crc, String jsonB public Response importDatasetDdi(@Context ContainerRequestContext crc, String xml, @PathParam("identifier") String parentIdtf, @QueryParam("pid") String pidParam, @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.isPowerUser((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); + } Dataset ds = null; try { ds = jsonParser().parseDataset(importService.ddiToJson(xml)); @@ -656,10 +656,10 @@ public Response importDatasetDdi(@Context ContainerRequestContext crc, String xm public Response recreateDataset(@Context ContainerRequestContext crc, String jsonLDBody, @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.isPowerUser((AuthenticatedUser) u, owner))) { + return error(Status.FORBIDDEN, "Not a superuser or power user"); + } Dataset ds = new Dataset(); @@ -1634,8 +1634,8 @@ public Response listLinks(@Context ContainerRequestContext crc, @PathParam("iden try { User u = getRequestUser(crc); Dataverse dv = findDataverseOrDie(dvIdtf); - if (!u.isSuperuser()) { - return error(Status.FORBIDDEN, "Not a superuser"); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, dv))) { + return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } List dvsThisDvHasLinkedToList = dataverseSvc.findDataversesThisIdHasLinkedTo(dv.getId()); @@ -2213,7 +2213,7 @@ public Response setStorageDriver(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); if (!permissionSvc.isPowerUser(user, dataverse)) { - return error(Response.Status.FORBIDDEN, "Superusers or Power Admins only."); + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -2236,8 +2236,8 @@ public Response resetStorageDriver(@Context ContainerRequestContext crc, @PathPa Dataverse dataverse = findDataverseOrDie(id); try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!user.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "Superusers only."); + if (!permissionSvc.isPowerUser(user, dataverse)) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { return wr.getResponse(); 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 0a1b19985a4..b0eb3537beb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -729,8 +729,9 @@ public Response reingest(@Context ContainerRequestContext crc, @PathParam("id") AuthenticatedUser u; try { u = getRequestAuthenticatedUserOrDie(crc); - if (!u.isSuperuser()) { - return error(FORBIDDEN, "This API call can be used by superusers only"); + DataFile dataFile = findDataFileOrDie(id); + if (!permissionSvc.isPowerUser(u, dataFile)) { + return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -820,12 +821,10 @@ public Response redetectDatafile(@Context ContainerRequestContext crc, @PathPara public Response extractNcml(@Context ContainerRequestContext crc, @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.isPowerUser(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 e4300099244..c14ee0a5d63 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -156,15 +156,6 @@ public Response harvestingClient(@Context ContainerRequestContext crc, @PathPara public Response createHarvestingClient(@Context ContainerRequestContext crc, String jsonBody, @PathParam("nickName") String nickName, @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); @@ -200,6 +191,11 @@ public Response createHarvestingClient(@Context ContainerRequestContext crc, Str if (ownerDataverse == null) { return error(Response.Status.BAD_REQUEST, "No such dataverse: " + dataverseAlias); } + + User u = getRequestUser(crc); + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((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); @@ -241,29 +237,16 @@ public Response createHarvestingClient(@Context ContainerRequestContext crc, Str public Response modifyHarvestingClient(@Context ContainerRequestContext crc, String jsonBody, @PathParam("nickName") String nickName, @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.isPowerUser((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(); @@ -329,46 +312,35 @@ public Response deleteHarvestingClient(@Context ContainerRequestContext crc, @Pa try { User u = getRequestUser(crc); - if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { - throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete harvesting clients.")); + HarvestingClient harvestingClient = harvestingClientService.findByNickname(nickName); + if (harvestingClient == null) { + return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); + } + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((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)"); + } + + if (harvestingClient.isHarvestingNow()) { + 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): + try { + harvestingClientService.deleteClient(harvestingClient.getId()); + } catch (Exception ex) { + return error( Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); + } + + return ok("Harvesting Client " + nickName + ": delete in progress"); + } 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); - } - - 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 (harvestingClient.isDeleteInProgress()) { - 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"); - } - - // 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 ok("Harvesting Client " + nickName + ": delete in progress"); } @@ -390,16 +362,16 @@ public Response startHarvestingJob(@Context ContainerRequestContext crc, @PathPa 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.isPowerUser(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 32d942f972c..d2dd825061f 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; @@ -48,7 +49,7 @@ public class Pids extends AbstractApiBean { @Produces(MediaType.APPLICATION_JSON) public Response getPid(@Context ContainerRequestContext crc, @QueryParam("persistentId") String persistentId) { User user = getRequestUser(crc); - if (!user.isSuperuser()) { + if (!user.isAuthenticated() || permissionSvc.isPowerUser((AuthenticatedUser) user,)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser")); } 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 572b7fc5b78..597c5fa4229 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -1594,7 +1594,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.isPowerUser(dvRequest.getAuthenticatedUser(), workingVersion.getDataset()) : false; finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace, tabIngest, ignoreUploadFileLimits); if (nFiles != finalFileList.size()) { @@ -2065,7 +2065,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.isPowerUser((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..748210744be 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().isPowerUser((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..9951395c341 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().isPowerUser((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..cf56c38b9c6 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().isPowerUser((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..4f082018dc4 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().isPowerUser((AuthenticatedUser) getUser(), getDataset())) { + throw new IllegalCommandException("Only superusers or those with scopedPowerAdmin 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..342eb6bcf2d 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 @@ -34,8 +34,8 @@ 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"), + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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..71f3fe5935f 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().isPowerUser((AuthenticatedUser) getUser(), doomed))) { + throw new PermissionException("Destroy can only be called by superusers or users with ScopedPowerAdmin 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..84b11605721 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 @@ -34,7 +34,7 @@ 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() ) ) { + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), targetDataset) ) ) { throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.superusersonly"), this, null, targetDataset); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseLinkingDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseLinkingDataverseCommand.java index 9eddfcd2b9c..701c0241239 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseLinkingDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseLinkingDataverseCommand.java @@ -42,8 +42,8 @@ public DeleteDataverseLinkingDataverseCommand(DataverseRequest aRequest, Dataver @Override public Dataverse execute(CommandContext ctxt) throws CommandException { - if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException("Delete dataverse linking dataverse can only be called by superusers.", + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), editedDv))) { + throw new PermissionException("Delete dataverse linking dataverse can only be called by superusers or power users.", this, Collections.singleton(Permission.DeleteDataverse), editedDv); } Dataverse merged = ctxt.em().merge(editedDv); 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..e6d41b88d52 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().isPowerUser((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..45b78fefab2 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 @@ -70,8 +70,8 @@ public DestroyDatasetCommand(Dataset doomed, DataverseRequest aRequest) { 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.", + if ( doomed.isReleased() && (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), doomed) ) ) { + throw new PermissionException("Destroy can only be called by superusers or users with ScopedPowerAdmin 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/LinkDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java index 2e1aecc9a84..0d81532f7d5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LinkDataverseCommand.java @@ -45,8 +45,8 @@ public LinkDataverseCommand(DataverseRequest aRequest, Dataverse dataverse, Data @Override public DataverseLinkingDataverse execute(CommandContext ctxt) throws CommandException { - if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException("Link Dataverse can only be called by superusers.", + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), linkingDataverse))) { + throw new PermissionException("Link Dataverse can only be called by superusers or power users.", this, Collections.singleton(Permission.LinkDataverse), linkingDataverse); } if (linkedDataverse.equals(linkingDataverse)) { 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..0b2a34c4986 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().isPowerUser((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..6efd82805b0 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().isPowerUser((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..6037001de39 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 @@ -63,8 +63,8 @@ public void executeImpl(CommandContext ctxt) throws CommandException { 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())), + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((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..bc5813cd181 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().isPowerUser((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..e7420bcdef1 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 @@ -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().isPowerUser((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..313a531d7e0 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().isPowerUser((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..2f03855b009 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 @@ -35,8 +35,8 @@ 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"), + if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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/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..2b0ebf0dba0 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; @@ -48,8 +49,8 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { 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, + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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..a29b723f3d0 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().isPowerUser((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..c7aa1e4beaf 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 @@ -104,8 +104,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().isPowerUser((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 +128,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().isPowerUser((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 2a288e22dac..e99e083dc2d 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 @@ -57,7 +57,7 @@ protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandExcep // 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 (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse) && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { String executable = ctxt.systemConfig().getDataverseValidationExecutable(); boolean result = validateDataverseMetadataExternally(dataverse, executable, getRequest()); @@ -67,7 +67,7 @@ protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandExcep } } } - if (!getUser().isSuperuser() && updatedDataverseDTO != null) { + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse)) && 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 14d17dcd900..e0f9eb64227 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 @@ -37,8 +37,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().isPowerUser((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..c858c334a5d 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,6 +32,10 @@ public UpdateHarvestingClientCommand(DataverseRequest aRequest, HarvestingClient @Override public HarvestingClient execute(CommandContext ctxt) throws CommandException { + if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dv))) { + throw new PermissionException(BundleUtil.getStringFromBundle("command.exception.only.powerusers", Arrays.asList(this.toString())), + this, Collections.singleton(Permission.EditDataverse), dv); + } // 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 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..fe11b448c54 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().isPowerUser((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/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 8ee646488c1..e92929fe35d 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1011,13 +1011,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 scopedPowerAdmin 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 scopedPowerAdmin 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) @@ -2768,7 +2768,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 scopedPowerAdmin permission for this collection. permission.role.not.created.alias.already.exists=Role with this alias already exists. #ManageFilePermissionsPage.java @@ -2818,10 +2818,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 scopedPowerAdmin 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 scopedPowerAdmin permission for this item. #Admin-API admin.api.auth.mustBeSuperUser=Forbidden. You must be a superuser. @@ -2848,7 +2850,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 scopedPowerAdmin 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. @@ -2915,7 +2917,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 scopedPowerAdmin 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: 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 @@ - + - + - + - + - - - - + + + + From 05cdf8fc7e1b4aa8bede5a178ad4f60d93e3ceb3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 8 Sep 2026 14:04:46 -0400 Subject: [PATCH 03/11] merge fixes, cleanup, comment changes --- .../iq/dataverse/HarvestingClientsPage.java | 2 + .../iq/dataverse/HarvestingSetsPage.java | 2 + .../edu/harvard/iq/dataverse/api/Admin.java | 2 +- .../harvard/iq/dataverse/api/Datasets.java | 40 +++++++------- .../edu/harvard/iq/dataverse/api/Files.java | 6 +-- .../iq/dataverse/api/HarvestingClients.java | 54 +++++++++---------- .../edu/harvard/iq/dataverse/api/Pids.java | 2 +- .../impl/DeleteCollectionQuotaCommand.java | 4 +- .../impl/DeleteDatasetQuotaCommand.java | 6 +-- .../command/impl/DestroyDatasetCommand.java | 2 +- .../command/impl/MoveDataverseCommand.java | 2 +- .../impl/SetCollectionQuotaCommand.java | 4 +- .../command/impl/SetDatasetQuotaCommand.java | 8 +-- .../command/impl/UningestFileCommand.java | 2 +- .../impl/UpdateDataverseAttributeCommand.java | 1 + .../command/impl/UpdateDataverseCommand.java | 6 ++- .../settings/SettingsServiceBean.java | 2 +- 17 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java b/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java index d64380e8d58..c955ce48116 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingClientsPage.java @@ -110,6 +110,8 @@ public List completeSelectedDataverse(String query) { public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); + } else if (!isSuperUser()) { + return navigationWrapper.notAuthorized(); } if (dataverseId != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java index e2a7a58bf79..0b66b652e0c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java @@ -123,6 +123,8 @@ public void setOaiServerStatusRadio(int oaiServerStatusRadio) { public String init() { if (!isSessionUserAuthenticated()) { return "/loginpage.xhtml" + navigationWrapper.getRedirectPage(); + } else if (!isSuperUser()) { + return navigationWrapper.notAuthorized(); } configuredHarvestingSets = oaiSetService.findAll(); 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 c60b67a62a2..d4cbcb533bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -2506,7 +2506,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) { 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 331085cfdf2..7ce03f86be6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -379,7 +379,7 @@ 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) || !permissionSvc.isPowerUser((AuthenticatedUser) u, doomed))) { @@ -1500,7 +1500,7 @@ 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.") @@ -1742,7 +1742,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet boolean badFiles = false; boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); for (DataFile datafile : filesToEmbargo) { - // superuser can overrule an existing embargo, even on released files + // power users can overrule an existing embargo, even on released files if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; @@ -1777,7 +1777,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet embargoService.deleteById(emb.getId(), authenticatedUser.getIdentifier()); } } - //If superuser, report changes to any released files + //If poweruser, report changes to any released files if (permissionSvc.isPowerUser(authenticatedUser, dataset)) { String releasedFiles = filesToEmbargo.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); @@ -1868,7 +1868,7 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet boolean badFiles = false; boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); for (DataFile datafile : embargoFilesToUnset) { - // superuser can overrule an existing embargo, even on released files + // 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; @@ -2045,7 +2045,7 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param boolean badFiles = false; boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); for (DataFile datafile : filesToRetention) { - // superuser can overrule an existing retention, even on released files + // power users can overrule an existing retention, even on released files if (datafile.isReleased() && !isPowerUser) { restrictedFiles.add(datafile.getId()); badFiles = true; @@ -2080,7 +2080,7 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param retentionService.delete(ret, authenticatedUser.getIdentifier()); } } - //If superuser, report changes to any released files + //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(",")); @@ -2229,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) { @@ -2262,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 @@ -4020,7 +4020,7 @@ 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.") @@ -4092,7 +4092,7 @@ 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 { @@ -4114,7 +4114,7 @@ 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, @@ -5383,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, @@ -5415,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, @@ -5477,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, @@ -5920,7 +5920,7 @@ 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.") @@ -6030,7 +6030,7 @@ 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 { AuthenticatedUser user; try { @@ -6088,7 +6088,7 @@ 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.") @@ -6606,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, @@ -6648,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, 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 d9a20677a7a..4fcacb825d1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -701,7 +701,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,7 +761,7 @@ 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; @@ -863,7 +863,7 @@ 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); 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 2fb458bf995..ae302813ae0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -344,38 +344,32 @@ 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. - + User u = getRequestUser(crc); + HarvestingClient harvestingClient = harvestingClientService.findByNickname(nickName); + if (harvestingClient == null) { + return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); + } + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((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)"); + } + + if (harvestingClient.isHarvestingNow()) { + 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): try { - User u = getRequestUser(crc); - HarvestingClient harvestingClient = harvestingClientService.findByNickname(nickName); - if (harvestingClient == null) { - return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); - } - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((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)"); - } - - if (harvestingClient.isHarvestingNow()) { - 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): - try { - harvestingClientService.deleteClient(harvestingClient.getId()); - } catch (Exception ex) { - return error( Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); - } - - return ok("Harvesting Client " + nickName + ": delete in progress"); - - } catch (WrappedResponse wr) { - return wr.getResponse(); + harvestingClientService.deleteClient(harvestingClient.getId()); + } catch (Exception ex) { + return error(Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); } + + return ok("Harvesting Client " + nickName + ": delete in progress"); } 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 687fbb57735..8ea55d63326 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Pids.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Pids.java @@ -58,7 +58,7 @@ public Response getPid(@Context ContainerRequestContext crc, @Parameter(description = "Persistent identifier to query.") @QueryParam("persistentId") String persistentId) { User user = getRequestUser(crc); - if (!user.isAuthenticated() || permissionSvc.isPowerUser((AuthenticatedUser) user,)) { + if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser")); } 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 342eb6bcf2d..c17fa47dfdb 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,7 +33,7 @@ public DeleteCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target) @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser + // first check if user is a power user if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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/DeleteDatasetQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetQuotaCommand.java index 84b11605721..b729595889c 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 + // first check if user is a power user if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), targetDataset) ) ) { - throw new PermissionException(BundleUtil.getStringFromBundle("dataset.storage.quota.superusersonly"), + 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/DestroyDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java index 45b78fefab2..226043e430c 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,7 +69,7 @@ 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 + // first check if dataset is released, and if so, if user is a power user if ( doomed.isReleased() && (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), doomed) ) ) { throw new PermissionException("Destroy can only be called by superusers or users with ScopedPowerAdmin permission on the dataset.", this, Collections.singleton(Permission.DeleteDatasetDraft), doomed); 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 6037001de39..f596094b87b 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,7 +62,7 @@ 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 + // first check if user is a power user if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((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/SetCollectionQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java index 2f03855b009..a0566c1f70e 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,7 +34,7 @@ public SetCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target, Lo @Override public void executeImpl(CommandContext ctxt) throws CommandException { - // Check if user is a superuser: + // Check if user is a power user: if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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..d800c29e033 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().isPowerUser(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/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index 2b0ebf0dba0..f5592b7be0c 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 @@ -48,7 +48,7 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser + // first check if user is a power user if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((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/UpdateDataverseAttributeCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java index c7aa1e4beaf..4fa45155473 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; 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 9a16e12328f..d28cbff725d 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().isPowerUser((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() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse) && 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() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse)) && 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/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. From a75713c057b14857ac6e2bd361e1957552192b0c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 8 Sep 2026 14:38:26 -0400 Subject: [PATCH 04/11] update permission service calls --- .../iq/dataverse/PermissionServiceBean.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 801d8e387cf..8c3f26076bb 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 && isPowerUser((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) ){ @@ -398,7 +398,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 && isPowerUser((AuthenticatedUser) user, dvo)) { return true; } else if (!user.isAuthenticated()) { Set requiredCopy = EnumSet.copyOf(required); @@ -414,11 +414,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 (isPowerUser(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 +453,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 && isPowerUser((AuthenticatedUser) req.getUser(), dvo)) { return EnumSet.allOf(Permission.class); } @@ -479,7 +480,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 && isPowerUser((AuthenticatedUser) ra, dvo)) { return EnumSet.allOf(Permission.class); } @@ -505,7 +506,9 @@ public boolean isPowerUser(AuthenticatedUser user, DvObject dvo) { if (dvo == null) { return false; } - return hasPermissionsFor(user, dvo, EnumSet.of(Permission.ScopedPowerAdmin)); + Set ras = new HashSet<>(groupService.groupsFor(user, dvo)); + ras.add(user); + return hasGroupPermissionsFor(ras, dvo, EnumSet.of(Permission.ScopedPowerAdmin)); } private void addGroupPermissionsFor(Set ras, DvObject dvo, Set permissions) { @@ -1096,11 +1099,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 (isPowerUser(authUser, dvObject)) { return true; } - userAndGroups.add(user); + userAndGroups.add(authUser); } for (RoleAssignee ra : userAndGroups) { From 1a4930e8b96438a5701ee626faa928532f1273ad Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 8 Sep 2026 17:05:40 -0400 Subject: [PATCH 05/11] update docs, dashboard, cleanup, refactor, isPowerUserOnSomeDvObject call --- .../source/admin/collectionquotas.rst | 2 +- doc/sphinx-guides/source/admin/dashboard.rst | 2 +- .../source/admin/dataverses-datasets.rst | 26 +++---- .../source/admin/metadatacustomization.rst | 2 +- .../source/api/curation-labels.rst | 2 +- doc/sphinx-guides/source/api/native-api.rst | 28 ++++---- .../harvard/iq/dataverse/DashboardPage.java | 6 +- .../edu/harvard/iq/dataverse/DatasetPage.java | 3 +- .../dataverse/DataverseRoleServiceBean.java | 6 ++ .../iq/dataverse/EditDatafilesPage.java | 7 +- .../iq/dataverse/PermissionServiceBean.java | 47 +++++++++++-- .../iq/dataverse/PermissionsWrapper.java | 20 ++++-- .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../edu/harvard/iq/dataverse/api/Access.java | 4 +- .../edu/harvard/iq/dataverse/api/Admin.java | 22 +++--- .../harvard/iq/dataverse/api/Datasets.java | 68 +++++++++---------- .../harvard/iq/dataverse/api/Dataverses.java | 46 ++++++------- .../edu/harvard/iq/dataverse/api/Files.java | 6 +- .../iq/dataverse/api/HarvestingClients.java | 12 ++-- .../datadeposit/MediaResourceManagerImpl.java | 3 +- .../dashboard/DashboardMoveDatasetPage.java | 2 +- .../dashboard/DashboardMoveDataversePage.java | 2 +- .../datasetutility/AddReplaceFileHelper.java | 6 +- .../command/impl/AbstractDatasetCommand.java | 2 +- .../command/impl/CreateDataverseCommand.java | 2 +- .../command/impl/CreateRoleCommand.java | 2 +- .../CuratePublishedDatasetVersionCommand.java | 2 +- .../impl/DeleteCollectionQuotaCommand.java | 2 +- .../command/impl/DeleteDataFileCommand.java | 2 +- .../impl/DeleteDatasetQuotaCommand.java | 2 +- .../engine/command/impl/DeletePidCommand.java | 2 +- .../command/impl/DestroyDatasetCommand.java | 2 +- .../command/impl/ImportDatasetCommand.java | 5 +- .../impl/ListDataverseContentCommand.java | 2 +- .../command/impl/MoveDatasetCommand.java | 2 +- .../command/impl/MoveDataverseCommand.java | 2 +- .../command/impl/PublishDatasetCommand.java | 2 +- .../command/impl/PublishDataverseCommand.java | 4 +- .../command/impl/ReservePidCommand.java | 2 +- .../impl/SetCollectionQuotaCommand.java | 2 +- .../command/impl/SetDatasetQuotaCommand.java | 2 +- .../command/impl/UningestFileCommand.java | 2 +- .../impl/UpdateDatasetTargetURLCommand.java | 2 +- .../impl/UpdateDataverseAttributeCommand.java | 4 +- .../command/impl/UpdateDataverseCommand.java | 2 +- .../UpdateDvObjectPIDMetadataCommand.java | 3 +- .../impl/UpdateHarvestingClientCommand.java | 6 +- .../UpdatePublishedDatasetVersionCommand.java | 2 +- src/main/webapp/dashboard.xhtml | 6 +- src/main/webapp/dataset.xhtml | 2 +- src/main/webapp/dataverse.xhtml | 8 +-- src/main/webapp/dataverse_header.xhtml | 2 +- .../webapp/file-edit-button-fragment.xhtml | 4 +- 53 files changed, 219 insertions(+), 189 deletions(-) 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..12dc736a266 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 (though some tools are superuser only). If you are a logged-in superuser or power user, 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 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..c679d68a29a 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 ScopedPowerAdmin 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/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 126ee41dba2..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; @@ -1518,7 +1517,7 @@ public boolean isPowerUserOnDataset(){ return false; } - return permissionService.isPowerUser((AuthenticatedUser) session.getUser(), dataset); + return permissionService.isPowerUserOn((AuthenticatedUser) session.getUser(), dataset); } /** * Check Dataset related permissions 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 81c968a9048..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 = permissionService.isPowerUser((AuthenticatedUser) session.getUser(), dataset); + 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().isAuthenticated()) ? permissionService.isPowerUser((AuthenticatedUser) session.getUser(), dataset) : 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/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 8c3f26076bb..52c39cab9fd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -334,7 +334,7 @@ public List whichChildrenHasPermissionsFor(DataverseRequest req, DvObj User user = req.getUser(); // quick cases - if (user instanceof AuthenticatedUser && isPowerUser((AuthenticatedUser) user, dvo)) { + if (user instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) user, dvo)) { return children; // it's good to be king/power user } else if (!user.isAuthenticated()) { @@ -398,7 +398,7 @@ public List whichChildrenHasPermissionsForOrReleased(DataverseRequest public boolean hasPermissionsFor(DataverseRequest req, DvObject dvo, Set required) { User user = req.getUser(); - if (user instanceof AuthenticatedUser && isPowerUser((AuthenticatedUser) user, dvo)) { + if (user instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) user, dvo)) { return true; } else if (!user.isAuthenticated()) { Set requiredCopy = EnumSet.copyOf(required); @@ -415,7 +415,7 @@ public boolean hasPermissionsFor(DataverseRequest req, DvObject dvo, Set required) { if (ra instanceof AuthenticatedUser au) { - if (isPowerUser(au, dvo)) { + if (isPowerUserOn(au, dvo)) { return true; } } else if (ra instanceof User user) { @@ -453,7 +453,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() instanceof AuthenticatedUser && isPowerUser((AuthenticatedUser) req.getUser(), dvo)) { + if (req.getUser() instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) req.getUser(), dvo)) { return EnumSet.allOf(Permission.class); } @@ -480,7 +480,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 && isPowerUser((AuthenticatedUser) ra, dvo)) { + if (ra instanceof AuthenticatedUser && isPowerUserOn((AuthenticatedUser) ra, dvo)) { return EnumSet.allOf(Permission.class); } @@ -496,7 +496,7 @@ public Set permissionsFor(RoleAssignee ra, DvObject dvo) { return permissions; } - public boolean isPowerUser(AuthenticatedUser user, DvObject dvo) { + public boolean isPowerUserOn(AuthenticatedUser user, DvObject dvo) { if (user == null) { return false; } @@ -510,6 +510,39 @@ public boolean isPowerUser(AuthenticatedUser user, DvObject dvo) { ras.add(user); return hasGroupPermissionsFor(ras, dvo, EnumSet.of(Permission.ScopedPowerAdmin)); } + + 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 ScopedPowerAdmin permission + List powerRoles = roleService.findAllWithPermission(Permission.ScopedPowerAdmin); + 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)) { @@ -1100,7 +1133,7 @@ public boolean hasLocallyFAIRAccess(DataverseRequest req, DvObject dvObject) { Set userAndGroups = new HashSet<>(groupService.groupsFor(req)); User user = req.getUser(); if (user instanceof AuthenticatedUser authUser) { - if (isPowerUser(authUser, dvObject)) { + if (isPowerUserOn(authUser, dvObject)) { return true; } userAndGroups.add(authUser); diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 4e93f645d0c..f03dd91f8f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -14,9 +14,7 @@ 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; @@ -38,6 +36,9 @@ public class PermissionsWrapper implements java.io.Serializable { @EJB DatasetVersionServiceBean datasetVersionService; + @EJB + DataverseServiceBean dataverseService; + @Inject DataverseSession session; @@ -265,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 instanceof AuthenticatedUser && permissionService.isPowerUser((AuthenticatedUser) u, dvo)) { + 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 @@ -333,7 +334,18 @@ public boolean authUsersCanCreateDataversesInDataverse(Dataverse dataverse) { public boolean isPowerUserOn(DvObject dvo) { User u = session.getUser(); - return (u instanceof AuthenticatedUser && permissionService.isPowerUser((AuthenticatedUser) u, dvo)); + 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 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 8f9213518b3..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.isPowerUser(authenticatedUser, dvObject) || 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 1adc50fe213..b5e6f6556c4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -1934,7 +1934,7 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, } - if (!(permissionService.isPowerUser(dataverseRequest.getAuthenticatedUser(), dataFile) || 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 (!(permissionService.isPowerUser(dataverseRequest.getAuthenticatedUser(), dataFile) || 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 d4cbcb533bd..f652eefd721 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; @@ -1794,7 +1793,7 @@ public Response reregisterHdlToPID(@Context ContainerRequestContext crc, User u = getRequestUser(crc); Dataset ds = findDatasetOrDie(id); - if (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUser((AuthenticatedUser) u, ds)) { + if (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) u, ds)) { logger.info("Bad Request Unauthor " ); return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -1835,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."); } @@ -1948,7 +1950,7 @@ public Response registerDataFilesInCollection(@Context ContainerRequestContext c AuthenticatedUser user; try { user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, collection)) { + if (!permissionSvc.isPowerUserOn(user, collection)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { @@ -2148,7 +2150,7 @@ public Response computeDataFileHashValue(@Context ContainerRequestContext crc, try { User u = getRequestAuthenticatedUserOrDie(crc); DataFile fileToUpdate = findDataFileOrDie(fileId); - if (!permissionSvc.isPowerUser((AuthenticatedUser) u, fileToUpdate)) { + if (!permissionSvc.isPowerUserOn((AuthenticatedUser) u, fileToUpdate)) { return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse e1) { @@ -2215,7 +2217,7 @@ public Response validateDataFileHashValue(@Context ContainerRequestContext crc, try { User u = getRequestAuthenticatedUserOrDie(crc); DataFile fileToValidate = findDataFileOrDie(fileId); - if (!permissionSvc.isPowerUser((AuthenticatedUser) u, fileToValidate)) { + if (!permissionSvc.isPowerUserOn((AuthenticatedUser) u, fileToValidate)) { return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse e1) { @@ -2481,7 +2483,7 @@ public Response addRoleAssignementsToChildren(@Context ContainerRequestContext c AuthenticatedUser user = null; try { user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, owner)) { + if (!permissionSvc.isPowerUserOn(user, owner)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { @@ -2514,7 +2516,7 @@ public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Param } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, dataverse)) { + if (!permissionSvc.isPowerUserOn(user, dataverse)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { @@ -2542,7 +2544,7 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, dataverse)) { + if (!permissionSvc.isPowerUserOn(user, dataverse)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { @@ -2577,7 +2579,7 @@ public Response resetCurationLabelSet(@Context ContainerRequestContext crc, } try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, dataverse)) { + if (!permissionSvc.isPowerUserOn(user, dataverse)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } } catch (WrappedResponse wr) { 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 7ce03f86be6..1d37319b081 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -341,7 +341,7 @@ public Response deleteDataset(@Context ContainerRequestContext crc, @Parameter(d boolean destroy = false; if (doomed.getVersions().size() == 1) { - if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUser((AuthenticatedUser) u, doomed))) { + 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; @@ -382,7 +382,7 @@ public Response destroyDataset(@Context ContainerRequestContext crc, @Parameter( // first check if dataset is released, and if so, if user is a poweruser Dataset doomed = findDatasetOrDie(id); - if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUser((AuthenticatedUser) u, doomed))) { + if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) u, doomed))) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser"))); } @@ -1380,7 +1380,7 @@ public Response publishDataset(@Context ContainerRequestContext crc, isMinor = false; break; case "updatecurrent": - if (permissionSvc.isPowerUser(user, ds)) { + if (permissionSvc.isPowerUserOn(user, ds)) { updateCurrent = true; } else { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); @@ -1508,7 +1508,7 @@ public Response publishMigratedDataset(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); Dataset ds = findDatasetOrDie(id); - if (!permissionSvc.isPowerUser(user, ds)) { + if (!permissionSvc.isPowerUserOn(user, ds)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } try { @@ -1662,7 +1662,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet * check later. */ - if ((!permissionSvc.isPowerUser(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + 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"); } @@ -1740,7 +1740,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet if (datasetFiles.containsAll(filesToEmbargo)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; - boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : filesToEmbargo) { // power users can overrule an existing embargo, even on released files if (datafile.isReleased() && !isPowerUser) { @@ -1778,7 +1778,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @Paramet } } //If poweruser, report changes to any released files - if (permissionSvc.isPowerUser(authenticatedUser, dataset)) { + if (permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { String releasedFiles = filesToEmbargo.stream().filter(d -> d.isReleased()) .map(d -> d.getId().toString()).collect(Collectors.joining(",")); if (!releasedFiles.isBlank()) { @@ -1824,7 +1824,7 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet // 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 ((!permissionSvc.isPowerUser(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + 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 ScopedPowerAdmin permission, or user does not have EditDataset permissions"); } @@ -1866,7 +1866,7 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet if (datasetFiles.containsAll(embargoFilesToUnset)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; - boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : embargoFilesToUnset) { // power users can overrule an existing embargo, even on released files if (datafile.getEmbargo()==null || ((datafile.isReleased() && datafile.getEmbargo() != null) && !isPowerUser)) { @@ -1945,7 +1945,7 @@ 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 ((!permissionSvc.isPowerUser(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + 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 ScopedPowerAdmin permission, or user does not have EditDataset permissions"); } @@ -2043,7 +2043,7 @@ public Response createFileRetention(@Context ContainerRequestContext crc, @Param if (datasetFiles.containsAll(filesToRetention)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; - boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : filesToRetention) { // power users can overrule an existing retention, even on released files if (datafile.isReleased() && !isPowerUser) { @@ -2126,7 +2126,7 @@ 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 ((!permissionSvc.isPowerUser(authenticatedUser, dataset) && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + 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"); } @@ -2179,7 +2179,7 @@ public Response removeFileRetention(@Context ContainerRequestContext crc, @Param if (datasetFiles.containsAll(retentionFilesToUnset)) { JsonArrayBuilder restrictedFiles = JsonUtil.createArrayBuilder(); boolean badFiles = false; - boolean isPowerUser = permissionSvc.isPowerUser(authenticatedUser, dataset); + boolean isPowerUser = permissionSvc.isPowerUserOn(authenticatedUser, dataset); for (DataFile datafile : retentionFilesToUnset) { // superuser/power user can overrule an existing retention, even on released files if (datafile.getRetention()==null || ((datafile.isReleased() && datafile.getRetention() != null) && !isPowerUser)) { @@ -2249,7 +2249,7 @@ public Response updateDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } - if (!permissionSvc.isPowerUser(authenticatedUser, dataset)) { + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -2280,7 +2280,7 @@ public Response deleteDatasetFilesLimits(@Context ContainerRequestContext crc, return ex.getResponse(); } - if (!permissionSvc.isPowerUser(authenticatedUser, dataset)) { + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -2734,7 +2734,7 @@ public Response receiveChecksumValidationResults(@Context ContainerRequestContex String statusMessageFromDcm = jsonFromDcm.getString("status"); try { Dataset dataset = findDatasetOrDie(id); - if (!permissionSvc.isPowerUser(authenticatedUser, dataset)) { + if (!permissionSvc.isPowerUserOn(authenticatedUser, dataset)) { return error(Response.Status.FORBIDDEN, "Superusers or power users only."); } if ("validation passed".equals(statusMessageFromDcm)) { @@ -3031,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 instanceof AuthenticatedUser) || !permissionSvc.isPowerUser((AuthenticatedUser) user, dataset)) { + if (!(user instanceof AuthenticatedUser) || !permissionSvc.isPowerUserOn((AuthenticatedUser) user, dataset)) { Integer effectiveDatasetFileCountLimit = dataset.getEffectiveDatasetFileCountLimit(); boolean hasFileCountLimit = dataset.isDatasetFileCountLimitSet(effectiveDatasetFileCountLimit); if (hasFileCountLimit) { @@ -3650,7 +3650,7 @@ public Response deleteLocks(@Context ContainerRequestContext crc, @Parameter(des try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); Dataset dataset = findDatasetOrDie(id); - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -3702,7 +3702,7 @@ public Response lockDataset(@Context ContainerRequestContext crc, @Parameter(des try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); Dataset dataset = findDatasetOrDie(id); - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } DatasetLock lock = dataset.getLockFor(lockType); @@ -4041,7 +4041,7 @@ public Response setFileStore(@Context ContainerRequestContext crc, @Parameter(de return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -4079,7 +4079,7 @@ public Response resetFileStore(@Context ContainerRequestContext crc, @Parameter( return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -4101,7 +4101,7 @@ public Response getCurationLabelSet(@Context ContainerRequestContext crc, @Param AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); DataverseRequest req = createDataverseRequest(user); dataset = findDatasetUserCanSeeOrDie(dvIdtf, req,false); - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse ex) { @@ -4135,7 +4135,7 @@ public Response setCurationLabelSet(@Context ContainerRequestContext crc, return ex.getResponse(); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } if (SystemConfig.CURATIONLABELSDISABLED.equals(curationLabelSet) || SystemConfig.DEFAULTCURATIONLABELSET.equals(curationLabelSet)) { @@ -4177,7 +4177,7 @@ public Response resetCurationLabelSet(@Context ContainerRequestContext crc, @Par return ex.getResponse(); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -5394,7 +5394,7 @@ public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); DataverseRequest req = createDataverseRequest(au); Dataset ds = findDatasetUserCanSeeOrDie(datasetId, req, false); - if (!permissionSvc.isPowerUser(au, ds)) { + if (!permissionSvc.isPowerUserOn(au, ds)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, @@ -5429,7 +5429,7 @@ public Response setDatasetVersionArchivalStatus(@Context ContainerRequestContext try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); Dataset dataset = findDatasetOrDie(datasetId); - if (!permissionSvc.isPowerUser(au, dataset)) { + if (!permissionSvc.isPowerUserOn(au, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -5487,7 +5487,7 @@ public Response deleteDatasetVersionArchivalStatus(@Context ContainerRequestCont try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); Dataset ds = findDatasetOrDie(datasetId); - if (!permissionSvc.isPowerUser(au, ds)) { + if (!permissionSvc.isPowerUserOn(au, ds)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } DataverseRequest req = createDataverseRequest(au); @@ -5941,7 +5941,7 @@ public Response setguestbookEntryAtRequest(@Context ContainerRequestContext crc, return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, 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); @@ -5976,7 +5976,7 @@ public Response resetGuestbookEntryAtRequest(@Context ContainerRequestContext cr return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -6044,7 +6044,7 @@ public Response reconcilePid(@Context ContainerRequestContext crc, @Parameter(de } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } return response(req -> { @@ -6108,7 +6108,7 @@ public Response setPidGenerator(@Context ContainerRequestContext crc, @Parameter return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } if (PidUtil.getManagedProviderIds().contains(generatorId)) { @@ -6143,7 +6143,7 @@ public Response resetPidGenerator(@Context ContainerRequestContext crc, @Paramet return error(Response.Status.NOT_FOUND, "No such dataset"); } - if (!permissionSvc.isPowerUser(user, dataset)) { + if (!permissionSvc.isPowerUserOn(user, dataset)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -6622,7 +6622,7 @@ public Response addVersionNote(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); Dataset ds = findDatasetOrDie(datasetId); - if (!permissionSvc.isPowerUser(user, ds)) { + if (!permissionSvc.isPowerUserOn(user, ds)) { return forbidden(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } return response(req -> { @@ -6661,7 +6661,7 @@ public Response deleteVersionNote(@Context ContainerRequestContext crc, if (!DS_VERSION_DRAFT.equals(versionId)) { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); Dataset ds = findDatasetOrDie(datasetId); - if (!permissionSvc.isPowerUser(user, ds)) { + if (!permissionSvc.isPowerUserOn(user, ds)) { return forbidden(BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } 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 a89d66819ad..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,7 +427,7 @@ public Response createDataset(@Context ContainerRequestContext crc, return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.mustIncludeVersion")); } - if (!ds.getFiles().isEmpty() && !(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, owner))){ + if (!ds.getFiles().isEmpty() && !(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))){ return badRequest(BundleUtil.getStringFromBundle("dataverses.api.create.dataset.error.powerUserFiles")); } @@ -554,7 +553,7 @@ public Response importDataset(@Context ContainerRequestContext crc, try { User u = getRequestUser(crc); Dataverse owner = findDataverseOrDie(parentIdtf); - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, owner))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } Dataset ds = parseDataset(jsonBody, owner); @@ -643,11 +642,8 @@ 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.isPowerUser((AuthenticatedUser) u, owner))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } Dataset ds; @@ -728,7 +724,7 @@ public Response recreateDataset(@Context ContainerRequestContext crc, try { User u = getRequestUser(crc); Dataverse owner = findDataverseOrDie(parentIdtf); - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, owner))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, owner))) { return error(Status.FORBIDDEN, "Not a superuser or power user"); } @@ -1938,7 +1934,7 @@ public Response listLinks(@Context ContainerRequestContext crc, User u = getRequestUser(crc); DataverseRequest req = createDataverseRequest(u); Dataverse dv = findDataverseUserCanSeeOrDie(dvIdtf, req); - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, dv))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, dv))) { return error(Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } @@ -2644,7 +2640,7 @@ public Response setStorageDriver(@Context ContainerRequestContext crc, try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, dataverse)) { + if (!permissionSvc.isPowerUserOn(user, dataverse)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { @@ -2672,7 +2668,7 @@ public Response resetStorageDriver(@Context ContainerRequestContext crc, Dataverse dataverse = findDataverseOrDie(id); try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); - if (!permissionSvc.isPowerUser(user, dataverse)) { + if (!permissionSvc.isPowerUserOn(user, dataverse)) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { @@ -2719,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() @@ -2749,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); @@ -2778,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); } @@ -2810,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 4fcacb825d1..7fa395f4971 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; @@ -768,7 +766,7 @@ public Response reingest(@Context ContainerRequestContext crc, @Parameter(descri try { u = getRequestAuthenticatedUserOrDie(crc); DataFile dataFile = findDataFileOrDie(id); - if (!permissionSvc.isPowerUser(u, dataFile)) { + if (!permissionSvc.isPowerUserOn(u, dataFile)) { return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } } catch (WrappedResponse wr) { @@ -868,7 +866,7 @@ public Response extractNcml(@Context ContainerRequestContext crc, @Parameter(des try { AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); DataFile dataFileIn = findDataFileOrDie(id); - if (!permissionSvc.isPowerUser(au, dataFileIn)) { + if (!permissionSvc.isPowerUserOn(au, dataFileIn)) { return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); } java.nio.file.Path tempLocationPath = null; 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 ae302813ae0..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; @@ -216,7 +214,7 @@ public Response createHarvestingClient(@Context ContainerRequestContext crc, } User u = getRequestUser(crc); - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, ownerDataverse))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, ownerDataverse))) { return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can create harvesting clients."); } @@ -272,7 +270,7 @@ public Response modifyHarvestingClient(@Context ContainerRequestContext crc, if (harvestingClient == null) { return error( Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); } - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, harvestingClient.getDataverse()))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, harvestingClient.getDataverse()))) { return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can modify harvesting clients."); } @@ -349,7 +347,7 @@ public Response deleteHarvestingClient(@Context ContainerRequestContext crc, if (harvestingClient == null) { return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); } - if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUser((AuthenticatedUser) u, harvestingClient.getDataverse()))) { + if (!(u instanceof AuthenticatedUser && permissionSvc.isPowerUserOn((AuthenticatedUser) u, harvestingClient.getDataverse()))) { return error(Response.Status.UNAUTHORIZED, "Only superusers or power users can delete harvesting clients."); } @@ -403,7 +401,7 @@ public Response startHarvestingJob(@Context ContainerRequestContext crc, return error(Response.Status.NOT_FOUND, "No such client: "+clientNickname); } - if (!permissionSvc.isPowerUser(authenticatedUser, harvestingClient.getDataverse())) { + if (!permissionSvc.isPowerUserOn(authenticatedUser, harvestingClient.getDataverse())) { return error(Response.Status.FORBIDDEN, "Only admin or power users can run harvesting jobs"); } 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/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 c07c9a22d45..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 ? permissionService.isPowerUser(dvRequest.getAuthenticatedUser(), workingVersion.getDataset()) : 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 instanceof AuthenticatedUser) || !permissionService.isPowerUser((AuthenticatedUser) authUser, dataset)) { + 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 748210744be..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 @@ -320,7 +320,7 @@ protected void registerExternalVocabValuesIfAny(CommandContext ctxt, DatasetVers // To block Publishing dataset or Submitting dataset for review protected boolean getRequiresFilesToPublishOrReviewDataset(CommandContext ctxt) { - if (getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), getDataset())) { + 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 9951395c341..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() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), owner)) && 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 cf56c38b9c6..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,7 +35,7 @@ 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 && ctxt.permissions().isPowerUser((AuthenticatedUser) user, dv))) { + 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 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 4f082018dc4..e3364e563c4 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 @@ -53,7 +53,7 @@ public boolean isValidateLenient() { @Override public Dataset execute(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), getDataset())) { + if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), getDataset())) { throw new IllegalCommandException("Only superusers or those with scopedPowerAdmin permission on the dataset can curate published dataset versions", this); } Dataset savedDataset = null; 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 c17fa47dfdb..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 @@ -34,7 +34,7 @@ public DeleteCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target) @Override public void executeImpl(CommandContext ctxt) throws CommandException { // first check if user is a power user - if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), targetDataverse) ) ) { + 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 71f3fe5935f..1efc5d87bf8 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,7 +54,7 @@ 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) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), doomed))) { + 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 ScopedPowerAdmin permission on the file.", this, Collections.singleton(Permission.DeleteDatasetDraft), doomed); } 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 b729595889c..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 @@ -34,7 +34,7 @@ public DeleteDatasetQuotaCommand(DataverseRequest aRequest, Dataset target) { @Override public void executeImpl(CommandContext ctxt) throws CommandException { // first check if user is a power user - if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), targetDataset) ) ) { + 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/DeletePidCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java index e6d41b88d52..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,7 +36,7 @@ public DeletePidCommand(DataverseRequest request, Dataset dataset) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataset)) { + 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 226043e430c..637ca724d1b 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 @@ -70,7 +70,7 @@ public DestroyDatasetCommand(Dataset doomed, DataverseRequest aRequest) { protected void executeImpl(CommandContext ctxt) throws CommandException { // first check if dataset is released, and if so, if user is a power user - if ( doomed.isReleased() && (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), doomed) ) ) { + 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 ScopedPowerAdmin permission on the dataset.", this, Collections.singleton(Permission.DeleteDatasetDraft), 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 0b2a34c4986..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 @@ -33,7 +33,7 @@ public ListDataverseContentCommand(DataverseRequest aRequest, Dataverse anAffect @Override public List execute(CommandContext ctxt) throws CommandException { - if (getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dvToList)) { + 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 6efd82805b0..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,7 +64,7 @@ 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 && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), 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); } 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 f596094b87b..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 @@ -63,7 +63,7 @@ public void executeImpl(CommandContext ctxt) throws CommandException { boolean removeGuestbook = false, removeTemplate = false, removeFeatDv = false, removeMetadataBlock = false, removeLinkDv = false, removeLinkDs = false; // first check if user is a power user - if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), 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.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 bc5813cd181..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 (!(ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), theDataset) && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { + if (!(ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), theDataset) && ctxt.systemConfig().isExternalValidationAdminOverrideEnabled())) { String executable = ctxt.systemConfig().getDatasetValidationExecutable(); boolean result = validateDatasetMetadataExternally(theDataset, executable, getRequest()); 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 e7420bcdef1..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 (!(ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse) && 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 313a531d7e0..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,7 +31,7 @@ public ReservePidCommand(DataverseRequest request, Dataset dataset) { @Override public Dataset execute(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataset)) { + 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/SetCollectionQuotaCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCollectionQuotaCommand.java index a0566c1f70e..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 @@ -35,7 +35,7 @@ public SetCollectionQuotaCommand(DataverseRequest aRequest, Dataverse target, Lo @Override public void executeImpl(CommandContext ctxt) throws CommandException { // Check if user is a power user: - if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse) ) ) { + 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 d800c29e033..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 @@ -35,7 +35,7 @@ public SetDatasetQuotaCommand(DataverseRequest aRequest, Dataset target, Long al @Override public void executeImpl(CommandContext ctxt) throws CommandException { // Check if user is a poweruser: - if ( (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser(getRequest().getAuthenticatedUser(), dataset) ) ) { + 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/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index f5592b7be0c..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 @@ -49,7 +49,7 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { protected void executeImpl(CommandContext ctxt) throws CommandException { // first check if user is a power user - if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), uningest)) { + 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 a29b723f3d0..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,7 +34,7 @@ public UpdateDatasetTargetURLCommand( Dataset target, DataverseRequest aRequest) @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), target))) { + 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); } 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 4fa45155473..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 @@ -105,7 +105,7 @@ 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 && !(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse))) { + 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); } @@ -129,7 +129,7 @@ private void setBooleanAttribute(CommandContext ctxt, boolean adminOnly) throws } private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTypesIn) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse))) { + 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); } 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 d28cbff725d..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 @@ -55,7 +55,7 @@ public UpdateDataverseCommand(Dataverse dataverse, @Override protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandException { - boolean isPowerUser = getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dataverse); + 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: 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 e6a5ca1293f..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,7 +36,7 @@ public UpdateDvObjectPIDMetadataCommand(Dataset target, DataverseRequest aReques protected void executeImpl(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), target))) { + 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); } 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 c858c334a5d..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 @@ -32,11 +32,7 @@ public UpdateHarvestingClientCommand(DataverseRequest aRequest, HarvestingClient @Override public HarvestingClient execute(CommandContext ctxt) throws CommandException { - if (!(getUser() instanceof AuthenticatedUser && ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), dv))) { - throw new PermissionException(BundleUtil.getStringFromBundle("command.exception.only.powerusers", Arrays.asList(this.toString())), - this, Collections.singleton(Permission.EditDataverse), dv); - } - // 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 fe11b448c54..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 @@ -36,7 +36,7 @@ public UpdatePublishedDatasetVersionCommand(DataverseRequest aRequest, DatasetVe @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { // Check if the user is a superuser or power user - if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUser((AuthenticatedUser) getUser(), datasetVersion.getDataset())) { + 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); } 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.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 @@ - +
  • - +
  • From 2bdc7c0d748cb1082bc130ce4f3e7b279b9a8a12 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 09:59:48 -0400 Subject: [PATCH 06/11] power user, release note, flyway to add role --- doc/release-notes/10588-power-user-role.md | 10 ++++++++++ .../source/admin/dataverses-datasets.rst | 2 +- .../iq/dataverse/PermissionServiceBean.java | 6 +++--- .../harvard/iq/dataverse/api/Datasets.java | 4 ++-- .../authorization/DataverseRole.java | 2 +- .../dataverse/authorization/Permission.java | 2 +- .../CuratePublishedDatasetVersionCommand.java | 2 +- .../command/impl/DeleteDataFileCommand.java | 2 +- .../command/impl/DestroyDatasetCommand.java | 2 +- .../propertyFiles/BuiltInRoles.properties | 4 ++-- src/main/java/propertyFiles/Bundle.properties | 20 +++++++++---------- .../V6.10.1.2__10588-add-power-user-role.sql | 4 ++++ 12 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 doc/release-notes/10588-power-user-role.md create mode 100644 src/main/resources/db/migration/V6.10.1.2__10588-add-power-user-role.sql 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/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index c679d68a29a..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 or users with the ScopedPowerAdmin permission on the 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 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 diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 52c39cab9fd..9f30fddc3c2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -508,7 +508,7 @@ public boolean isPowerUserOn(AuthenticatedUser user, DvObject dvo) { } Set ras = new HashSet<>(groupService.groupsFor(user, dvo)); ras.add(user); - return hasGroupPermissionsFor(ras, dvo, EnumSet.of(Permission.ScopedPowerAdmin)); + return hasGroupPermissionsFor(ras, dvo, EnumSet.of(Permission.ScopedPowerUser)); } public boolean isPowerUserOnSomeDvObject(AuthenticatedUser user) { @@ -526,8 +526,8 @@ public boolean isPowerUserOnSomeDvObject(AuthenticatedUser user) { .collect(Collectors.toSet()); identifiers.add(user.getIdentifier()); - // Get all roles that have ScopedPowerAdmin permission - List powerRoles = roleService.findAllWithPermission(Permission.ScopedPowerAdmin); + // Get all roles that have ScopedPowerUser permission + List powerRoles = roleService.findAllWithPermission(Permission.ScopedPowerUser); if (powerRoles.isEmpty()) { return false; } 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 1d37319b081..298d97388d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1825,7 +1825,7 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @Paramet // 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 ((!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 ScopedPowerAdmin permission, or user does not have EditDataset permissions"); + 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 @@ -1946,7 +1946,7 @@ 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 ((!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 ScopedPowerAdmin permission, or user does not have EditDataset permissions"); + 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 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 b6674b4e4d3..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,7 +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_ADMIN = "powerAdmin"; + 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 d61eaafd591..bd66ccc91b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/Permission.java @@ -54,7 +54,7 @@ public enum Permission implements java.io.Serializable { // Delete DeleteDataverse(BundleUtil.getStringFromBundle("permission.deleteDataverse"), true, Dataverse.class), DeleteDatasetDraft(BundleUtil.getStringFromBundle("permission.deleteDataset"), true, Dataset.class), - ScopedPowerAdmin(BundleUtil.getStringFromBundle("permission.scopedPowerAdmin"), true, DvObject.class); + ScopedPowerUser(BundleUtil.getStringFromBundle("permission.scopedPowerUser"), true, DvObject.class); // FUTURE: 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 e3364e563c4..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 @@ -54,7 +54,7 @@ public boolean isValidateLenient() { @Override public Dataset execute(CommandContext ctxt) throws CommandException { if (!(getUser() instanceof AuthenticatedUser) || !ctxt.permissions().isPowerUserOn((AuthenticatedUser) getUser(), getDataset())) { - throw new IllegalCommandException("Only superusers or those with scopedPowerAdmin permission on the dataset can curate published dataset versions", this); + 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/DeleteDataFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java index 1efc5d87bf8..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 @@ -55,7 +55,7 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { //todo: clean this logic up! //for now, if called as destroy, will check for superuser acess 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 ScopedPowerAdmin permission on the file.", + 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/DestroyDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java index 637ca724d1b..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 @@ -71,7 +71,7 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { // 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 ScopedPowerAdmin permission on the dataset.", + 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/propertyFiles/BuiltInRoles.properties b/src/main/java/propertyFiles/BuiltInRoles.properties index b990611b85e..b39078a34d2 100644 --- a/src/main/java/propertyFiles/BuiltInRoles.properties +++ b/src/main/java/propertyFiles/BuiltInRoles.properties @@ -14,5 +14,5 @@ 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. -role.poweradmin.name=Power Admin -role.poweradmin.description=A person who can take any action on a dvobject that a superuser could. \ No newline at end of file +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 8e2613b6e15..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.powerusersonly=Only superusers and those with the scopedPowerAdmin permission on an item 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.powerusersonly=Only superusers and those with the scopedPowerAdmin permission on an item 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,7 +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.scopedPowerAdmin=Allow a user to take any action on a dvobject that a superuser could +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. @@ -2779,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.poweruser=Roles can only be created or edited by superusers or those with scopedPowerAdmin permission for this collection. +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 @@ -2829,12 +2829,12 @@ permission.cannotSaveRole=Cannot save role {0} pid.allowedCharacters=^[A-Za-z0-9._/:\\-]* #General Command Exception -command.exception.only.powerusers={1} can only be called by superusers or those with scopedPowerAdmin permission for this item. +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 scopedPowerAdmin permission for this item. +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. @@ -2861,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.mustBePowerUser=Forbidden. You must be a superuser or have the scopedPowerAdmin permission for this item. +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. @@ -2928,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.powerUserFiles=Only a superuser or those with the scopedPowerAdmin permission 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: @@ -3001,7 +3001,7 @@ permission.LinkDataset.label=LinkDataset permission.DeleteDataverse.label=DeleteDataverse permission.DeleteDatasetDraft.label=DeleteDatasetDraft permission.ManageFilePermissions.label=ManageFilePermissions -permission.ScopedPowerAdmin.label=Scoped Power Admin +permission.ScopedPowerUser.label=Scoped Power User permission.AddDataverse.desc=Add a dataverse within another dataverse permission.DeleteDatasetDraft.desc=Delete a dataset draft @@ -3019,7 +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.ScopedPowerAdmin.desc=Allow a user to take any action on a dvobject that a superuser could +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; From 01e9c8f0d232dd1df6bfe9a9f8b1780c5ccff0aa Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 11:06:15 -0400 Subject: [PATCH 07/11] more docs --- doc/sphinx-guides/source/admin/dashboard.rst | 2 +- doc/sphinx-guides/source/installation/config.rst | 2 ++ doc/sphinx-guides/source/user/dataset-management.rst | 2 ++ doc/sphinx-guides/source/user/dataverse-management.rst | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/dashboard.rst b/doc/sphinx-guides/source/admin/dashboard.rst index 12dc736a266..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 and power users (though some tools are superuser only). If you are a logged-in superuser or power user, 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 all parts of 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/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. From 6dcc62cd7cd0c048375f758eea7658536ad7f1b1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 13:25:27 -0400 Subject: [PATCH 08/11] tests --- .../dataverse/PermissionServiceBeanTest.java | 221 ++++++++++++++++++ .../iq/dataverse/PermissionsWrapperTest.java | 26 +++ 2 files changed, 247 insertions(+) create mode 100644 src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java 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..7cdeb8facfb --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java @@ -0,0 +1,221 @@ +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.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.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.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.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.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.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.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.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 testHasLocallyFAIRAccess_PowerUser() { + AuthenticatedUser user = mock(AuthenticatedUser.class); + when(user.isSuperuser()).thenReturn(false); + 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; From 453792f14acb047e08f683b3f906c4443ac120a4 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 13:37:34 -0400 Subject: [PATCH 09/11] update chichChildren to handle power assignment on child, update test --- .../iq/dataverse/PermissionServiceBean.java | 5 +- .../dataverse/PermissionServiceBeanTest.java | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 9f30fddc3c2..230be01c7c8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -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() ); } diff --git a/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java index 7cdeb8facfb..c12cfafce0a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/PermissionServiceBeanTest.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; @@ -22,6 +23,7 @@ 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; @@ -71,6 +73,7 @@ public void testIsPowerUserOn_NullDvObject() { 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(); @@ -92,6 +95,7 @@ public void testIsPowerUserOn_WithAssignment() { 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(); @@ -118,6 +122,7 @@ public void testIsPowerUserOn_WithParentAssignment() { 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); @@ -138,6 +143,7 @@ public void testIsPowerUserOn_BlockedNormalAdmin() { 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); @@ -154,6 +160,7 @@ public void testHasPermissionsFor_PowerUser() { 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); @@ -171,6 +178,7 @@ public void testPermissionsFor_PowerUser() { 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(); @@ -198,10 +206,60 @@ public void testWhichChildrenHasPermissionsFor_PowerUser() { 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(); From db4cf76e32fa80b9341665ff0b59f26c715a3c02 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 14:35:37 -0400 Subject: [PATCH 10/11] add mocks to fix tests --- .../command/impl/CreateDataverseCommandTest.java | 10 ++++++++++ .../engine/command/impl/CreateRoleCommandTest.java | 11 +++++++++++ .../engine/command/impl/MoveDataverseCommandTest.java | 10 ++++++++++ .../impl/SubmitDatasetForReviewCommandTest.java | 10 ++++++++++ 4 files changed, 41 insertions(+) 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(); + } + } ); } From ec3b3b4b9a281231fe0635ba3e37f76ca990931b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 9 Sep 2026 15:37:04 -0400 Subject: [PATCH 11/11] restore error responses, but FORBIDDEN for not power user --- .../edu/harvard/iq/dataverse/api/Admin.java | 26 ++++++++++--------- .../edu/harvard/iq/dataverse/api/Files.java | 11 ++++---- 2 files changed, 19 insertions(+), 18 deletions(-) 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 f652eefd721..af797ae768e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -2147,17 +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); - DataFile fileToUpdate = findDataFileOrDie(fileId); - if (!permissionSvc.isPowerUserOn((AuthenticatedUser) u, fileToUpdate)) { - return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); - } + 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) { @@ -2165,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."); } @@ -2214,17 +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); - DataFile fileToValidate = findDataFileOrDie(fileId); - if (!permissionSvc.isPowerUserOn((AuthenticatedUser) u, fileToValidate)) { - return error(Status.UNAUTHORIZED, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); - } + 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) { @@ -2232,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."); } 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 7fa395f4971..43973e2f990 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -761,18 +761,13 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @Paramete @Operation(summary = "Reingests a data file", 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); - DataFile dataFile = findDataFileOrDie(id); - if (!permissionSvc.isPowerUserOn(u, dataFile)) { - return error(FORBIDDEN, BundleUtil.getStringFromBundle("api.auth.mustBePowerUser")); - } } catch (WrappedResponse wr) { return wr.getResponse(); } - + DataFile dataFile; try { dataFile = findDataFileOrDie(id); @@ -780,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) {