diff --git a/doc/Architecture/privateurl.md b/doc/Architecture/privateurl.md new file mode 100644 index 00000000000..fdec655b63f --- /dev/null +++ b/doc/Architecture/privateurl.md @@ -0,0 +1,34 @@ +Private URL Design Doc +---------------------- + +The Private URL feature has been implemented as a specialized role assignment with an associated token that permits read-only access to the metadata and all files (regardless of if the files are restricted or not) of a draft version of a dataset. + +The primary use case for a Private URL is for journal editors to send a link to reviewers of a dataset before publication. In most cases, these journal editors do not permit depositors to publish on their own, which is to say they only allow depositors to have the "Contributor" role on the datasets they create. With only the "Contributor" role, depositors are unable to grant even read-only access to any user within the Dataverse installation and must contact the journal editor to make any adjustments to permissions, which they can't even see. This is all by design because it is the journal editor, not the depositor, who is in charge of both the security of the dataset and the timing of when the dataset is published. + +A secondary use case for a Private URL is for depositors who have the ability to manage permissions on their dataset (depositors who have the "Curator" or "Admin" role, which grants much more power than the "Contributor" role) to send a link to coauthors or other trusted parties to preview the dataset before the depositors publish the dataset on their own. For better security, these depositors could ask their coauthors to create Dataverse accounts and assign roles to them directly, rather than using a Private URL which requires no username or password. + +The token associated with the Private URL role assignment that can be used either in the GUI or via the API to elevate privileges beyond what a "Guest" can see. The ability to use a Private URL token via API was added mostly to facilitate automated testing of the feature but the far more common case is expected to be use of the Private URL token in a link that is clicked to open a browser, similar to links shared via Dropbox, Google, etc. + +When reviewers click a Private URL their browser sessions are set to the "PrivateUrlUser" that has the "Member" role only on the dataset in question and redirected to that dataset, where they will see an indication in blue at the top of the page that they are viewing an unpublished dataset. If the reviewer happens to be logged into Dataverse already, clicking the link will log them out because the review is meant to be blind. Because the dataset is always in draft when a Private URL is in effect, no downloads or any other activity by the reviewer are logged to the guestbook. All reviewers click the same Private URL containing the same token, and with the exception of an IP address being logged, it should be impossible to trace which reviewers have clicked a Private URL. If the reviewer navigates to the home page, the session is set to the Guest user and they will see what a Guest would see. + +The "Member" role is used because it contains the necessary read-only permissions, which are ViewUnpublishedDataset and DownloadFile. (Technically, the "Member" role also has the ViewUnpublishedDataverse permission but because the role is assigned at the dataset level and dataverses cannot be children of datasets, this permission has no effect.) Reusing the "Member" role helps contain the list of roles available at the dataset level to a reasonable number (five). + +Because the PrivateUrlUser has the "Member" role, all the same permissions apply. This means that the PrivateUrlUser (the reviewer, typically) can download all files, even if they have been restricted, across any dataset version. A Member can also download restricted files that have been deleted from previously published versions. + +Likewise, when a Private URL token is used via API, commands are executed using the "PrivateUrlUser" that has the "Member" role only on the dataset in question. This means that read-only operations such as downloads of the dataset's files are permitted. The Search API does not respect the Private URL token but you can download unpublished metadata using the Native API and download files using the Access API. + +A Private URL cannot be created for a published version of a dataset. In the GUI, you will be reminded of this fact with a popup. The API will explain this as well. + +If a draft dataset containing a Private URL is published, the Private URL is deleted. This means that reviewers who click the link after publication will see a 404. + +If a post-publication draft containing a Private URL is deleted, the Private URL is deleted. This is to ensure that if a new draft is created in the future, a new token will be used. + +The creation and deletion of a Private URL are limited to the "Curator" and "Admin" roles because only those roles have the permission called "ManageDatasetPermissions", which is the permission used by the "AssignRoleCommand" and "RevokeRoleCommand" commands. If you have the permission to create or delete a Private URL, the fact that a Private URL is enabled for a dataset will be indicated in blue at the top of the page. Success messages are shown at the top of the page when you create or delete a Private URL. In the GUI, deleting a Private URL is called "disabling" and you will be prompted for a confirmation. No matter what you call it the role is revoked. You can also delete a Private URL by revoking the role. + +A "Contributor" does not have the "ManageDatasetPermissions" permission and cannot see "Permissions" nor "Private URL" under the "Edit" menu of their dataset. When a Curator or Admin has enabled a Private URL on a Contributor's dataset, the Contributor does not see a visual indication that a Private URL has been enabled for their dataset. + +There is no way for an "Admin" or "Curator" to see when a Private URL was created or deleted for a dataset but someone who has access to the database can see that the following commands are logged to the "actionlogrecord" database table: CreatePrivateUrlCommand, DeletePrivateUrlCommand, and GetPrivateUrlCommand. + +See also + +* Private URL To Unpublished Dataset BRD: https://docs.google.com/document/d/1FT47QkZKcmjSgRnePaJO2g1nzcotLyN3Yb2ORvBr6cs/edit?usp=sharing diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 9fb67fc73ee..c0b0b3a7344 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -162,6 +162,21 @@ Restores the default logic of the field type to be used as the citation date. Sa DELETE http://$SERVER/api/datasets/$id/citationdate?key=$apiKey +List all the role assignments at the given dataset:: + + GET http://$SERVER/api/datasets/$id/assignments?key=$apiKey + +Create a Private URL (must be able to manage dataset permissions):: + + POST http://$SERVER/api/datasets/$id/privateUrl?key=$apiKey + +Get a Private URL from a dataset (if available):: + + GET http://$SERVER/api/datasets/$id/privateUrl?key=$apiKey + +Delete a Private URL from a dataset (if it exists):: + + DELETE http://$SERVER/api/datasets/$id/privateUrl?key=$apiKey Builtin Users ~~~~~ diff --git a/scripts/database/upgrades/privateurl.sql b/scripts/database/upgrades/privateurl.sql new file mode 100644 index 00000000000..67caf1f30b1 --- /dev/null +++ b/scripts/database/upgrades/privateurl.sql @@ -0,0 +1,2 @@ +-- A Private URL is a specialized role assignment with a token. +ALTER TABLE roleassignment ADD COLUMN privateurltoken character varying(255); diff --git a/src/main/java/Bundle.properties b/src/main/java/Bundle.properties index 5a6d571d06e..af870e7c3ab 100755 --- a/src/main/java/Bundle.properties +++ b/src/main/java/Bundle.properties @@ -750,6 +750,7 @@ dataset.editBtn.itemLabel.upload=Files (Upload) dataset.editBtn.itemLabel.metadata=Metadata dataset.editBtn.itemLabel.terms=Terms dataset.editBtn.itemLabel.permissions=Permissions +dataset.editBtn.itemLabel.privateUrl=Private URL dataset.editBtn.itemLabel.deleteDataset=Delete Dataset dataset.editBtn.itemLabel.deleteDraft=Delete Draft Version dataset.editBtn.itemLabel.deaccession=Deaccession Dataset @@ -862,6 +863,20 @@ dataset.mixedSelectedFilesForDownload=The restricted file(s) selected may not be dataset.downloadUnrestricted=Click Continue to download the files you have access to download. dataset.requestAccessToRestrictedFiles=You may request access to the restricted file(s) by clicking the Request Access button. +dataset.privateurl.infoMessageAuthor=Private URL is in use for this dataset: {0} +dataset.privateurl.infoMessageReviewer=This is an unpublished draft of a dataset that has been shared privately. +dataset.privateurl.header=Private URL +dataset.privateurl.tip=Use a Private URL to allow those without Dataverse accounts to access your unpublished dataset. +dataset.privateurl.absent=Private URL has not been created. +dataset.privateurl.createPrivateUrl=Create Private URL +dataset.privateurl.disablePrivateUrl=Disable Private URL +dataset.privateurl.disableConfirmationTitle=Confirm Disable Private URL +dataset.privateurl.disableConfirmationText=If you have sent the Private URL to others they will no longer be able to use it to access your unpublished dataset. +dataset.privateurl.cannotCreate=Private URL can only be used with unpublished versions of datasets. +dataset.privateurl.roleassigeeTitle=Private URL Enabled +dataset.privateurl.createdSuccess=Private URL created: {0} +dataset.privateurl.disabledSuccess=Private URL disabled. +dataset.privateurl.noPermToCreate=To create a Private URL you must have the following permissions: {0}. file.count={0} {0, choice, 0#Files|1#File|2#Files} file.count.selected={0} {0, choice, 0#Files Selected|1#File Selected|2#Files Selected} file.selectToAddBtn=Select Files to Add diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 2c460fcb486..396cdbbeed4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -6,14 +6,19 @@ import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.datavariable.VariableServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.impl.CreateDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateGuestbookResponseCommand; +import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeaccessionDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.DeletePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.DestroyDatasetCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetPrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.LinkDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; @@ -21,6 +26,9 @@ import edu.harvard.iq.dataverse.ingest.IngestRequest; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.metadataimport.ForeignMetadataImportServiceBean; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlUtil; import edu.harvard.iq.dataverse.search.SearchFilesServiceBean; import edu.harvard.iq.dataverse.search.SortBy; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -142,6 +150,10 @@ public enum DisplayMode { DatasetLinkingServiceBean dsLinkingService; @EJB SearchFilesServiceBean searchFilesService; + @EJB + DataverseRoleServiceBean dataverseRoleService; + @EJB + PrivateUrlServiceBean privateUrlService; @Inject DataverseRequestServiceBean dvRequestService; @Inject @@ -486,10 +498,19 @@ public boolean canDownloadFile(FileMetadata fileMetadata){ // -------------------------------------------------------------------- // -------------------------------------------------------------------- - // (2) Is user authenticated? - // No? Then no button... + // (2) In Dataverse 4.3 and earlier we required that users be authenticated + // to download files, but in developing the Private URL feature, we have + // added a new subclass of "User" called "PrivateUrlUser" that returns false + // for isAuthenticated but that should be able to download restricted files + // when given the Member role (which includes the DownloadFile permission). + // This is consistent with how Builtin and Shib users (both are + // AuthenticatedUsers) can download restricted files when they are granted + // the Member role. For this reason condition 2 has been changed. Previously, + // we required isSessionUserAuthenticated to return true. Now we require + // that the User is not an instance of GuestUser, which is similar in + // spirit to the previous check. // -------------------------------------------------------------------- - if (!(isSessionUserAuthenticated())){ + if (session.getUser() instanceof GuestUser){ this.fileDownloadPermissionMap.put(fid, false); return false; } @@ -1555,6 +1576,20 @@ public String init() { return "/404.xhtml"; } + try { + privateUrl = commandEngine.submit(new GetPrivateUrlCommand(dvRequestService.getDataverseRequest(), dataset)); + if (privateUrl != null) { + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.privateurl.infoMessageAuthor", Arrays.asList(getPrivateUrlLink(privateUrl)))); + } + } catch (CommandException ex) { + // No big deal. The user simply doesn't have access to create or delete a Private URL. + } + if (session.getUser() instanceof PrivateUrlUser) { + PrivateUrlUser privateUrlUser = (PrivateUrlUser) session.getUser(); + if (dataset != null && dataset.getId().equals(privateUrlUser.getDatasetId())) { + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.privateurl.infoMessageReviewer")); + } + } return null; } @@ -4282,4 +4317,44 @@ public String getSortByDescending() { return SortBy.DESCENDING; } + PrivateUrl privateUrl; + + public PrivateUrl getPrivateUrl() { + return privateUrl; + } + + public void setPrivateUrl(PrivateUrl privateUrl) { + this.privateUrl = privateUrl; + } + + public void createPrivateUrl() { + try { + PrivateUrl createdPrivateUrl = commandEngine.submit(new CreatePrivateUrlCommand(dvRequestService.getDataverseRequest(), dataset)); + privateUrl = createdPrivateUrl; + JH.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.privateurl.createdSuccess", Arrays.asList(getPrivateUrlLink(privateUrl)))); + } catch (CommandException ex) { + String msg = BundleUtil.getStringFromBundle("dataset.privateurl.noPermToCreate", PrivateUrlUtil.getRequiredPermissions(ex)); + logger.info("Unable to create a Private URL for dataset id " + dataset.getId() + ". Message to user: " + msg + " Exception: " + ex); + JH.addErrorMessage(msg); + } + } + + public void disablePrivateUrl() { + try { + commandEngine.submit(new DeletePrivateUrlCommand(dvRequestService.getDataverseRequest(), dataset)); + privateUrl = null; + JH.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.privateurl.disabledSuccess")); + } catch (CommandException ex) { + logger.info("CommandException caught calling DeletePrivateUrlCommand: " + ex); + } + } + + public boolean isUserCanCreatePrivateURL() { + return dataset.getLatestVersion().isDraft(); + } + + public String getPrivateUrlLink(PrivateUrl privateUrl) { + return privateUrl.getLink(); + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseSession.java b/src/main/java/edu/harvard/iq/dataverse/DataverseSession.java index edec5fbe008..3770c54750a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseSession.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseSession.java @@ -39,7 +39,7 @@ public User getUser() { return user; } - public void setUser(AuthenticatedUser aUser) { + public void setUser(User aUser) { logSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.SessionManagement,(aUser==null) ? "logout" : "login") .setUserIdentifier((aUser!=null) ? aUser.getIdentifier() : (user!=null ? user.getIdentifier() : "") )); diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index baac2a13037..3c78a9435aa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -12,6 +12,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.search.SearchServiceBean; import java.util.Map; @@ -23,6 +24,7 @@ import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.EnumSet; import java.util.logging.Level; import java.util.logging.Logger; @@ -127,7 +129,13 @@ public class EjbDataverseEngine { @EJB AuthenticationServiceBean authentication; - + + @EJB + SystemConfig systemConfig; + + @EJB + PrivateUrlServiceBean privateUrlService; + @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; @@ -371,7 +379,17 @@ public UserNotificationServiceBean notifications() { public AuthenticationServiceBean authentication() { return authentication; } - + + @Override + public SystemConfig systemConfig() { + return systemConfig; + } + + @Override + public PrivateUrlServiceBean privateUrl() { + return privateUrlService; + } + }; } diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java index 4c67c5b1342..e9cbe611656 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java @@ -399,7 +399,8 @@ private void rejectAccessToRequests(AuthenticatedUser au, List files) private boolean assignRole(RoleAssignee ra, DataFile file, DataverseRole r) { try { - commandEngine.submit(new AssignRoleCommand(ra, r, file, dvRequestService.getDataverseRequest())); + String privateUrlToken = null; + commandEngine.submit(new AssignRoleCommand(ra, r, file, dvRequestService.getDataverseRequest(), privateUrlToken)); JsfHelper.addSuccessMessage(r.getName() + " role assigned to " + ra.getDisplayInfo().getTitle() + " for " + file.getDisplayName() + "."); } catch (PermissionException ex) { JH.addMessage(FacesMessage.SEVERITY_ERROR, "The role was not able to be assigned.", "Permissions " + ex.getRequiredPermissions().toString() + " missing."); diff --git a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java index 36902669816..6c8e745e794 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManagePermissionsPage.java @@ -395,7 +395,8 @@ private void notifyRoleChange(RoleAssignee ra, UserNotification.Type type) { private void assignRole(RoleAssignee ra, DataverseRole r) { try { - commandEngine.submit(new AssignRoleCommand(ra, r, dvObject, dvRequestService.getDataverseRequest())); + String privateUrlToken = null; + commandEngine.submit(new AssignRoleCommand(ra, r, dvObject, dvRequestService.getDataverseRequest(), privateUrlToken)); JsfHelper.addSuccessMessage(r.getName() + " role assigned to " + ra.getDisplayInfo().getTitle() + " for " + dvObject.getDisplayName() + "."); // don't notify if role = file downloader and object is not released if (!(r.getAlias().equals(DataverseRole.FILE_DOWNLOADER) && !dvObject.isReleased()) ){ diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index de286c1dec1..955a7050ac5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -10,7 +10,9 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlUtil; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -63,7 +65,24 @@ void setup() { public RoleAssignee getRoleAssignee(String identifier) { switch (identifier.charAt(0)) { case ':': - return predefinedRoleAssignees.get(identifier); + /** + * This "startsWith" code in identifier2roleAssignee is here to + * support a functional requirement to display the Private URL + * role assignment when looking at permissions at the dataset + * level in the GUI and allow for revoking the role from that + * page. Interestingly, if you remove the "startsWith" code, + * null will be returned for Private URL but the assignment is + * still visible from the API. When null is returned + * ManagePermissionsPage cannot list the assignment. + * + * "startsWith" is the moral equivalent of + * "identifier.charAt(0)". :) + */ + if (identifier.startsWith(PrivateUrlUser.PREFIX)) { + return PrivateUrlUtil.identifier2roleAssignee(identifier); + } else { + return predefinedRoleAssignees.get(identifier); + } case '@': return authSvc.getAuthenticatedUser(identifier.substring(1)); case '&': diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssignment.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssignment.java index be3759e61d2..3d23bbb54c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssignment.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssignment.java @@ -39,6 +39,8 @@ query = "SELECT r FROM RoleAssignment r WHERE r.definitionPoint.id=:definitionPointId" ), @NamedQuery( name = "RoleAssignment.listByRoleId", query = "SELECT r FROM RoleAssignment r WHERE r.role=:roleId" ), + @NamedQuery( name = "RoleAssignment.listByPrivateUrlToken", + query = "SELECT r FROM RoleAssignment r WHERE r.privateUrlToken=:privateUrlToken" ), @NamedQuery( name = "RoleAssignment.deleteByAssigneeIdentifier_RoleIdDefinition_PointId", query = "DELETE FROM RoleAssignment r WHERE r.assigneeIdentifier=:userId AND r.role.id=:roleId AND r.definitionPoint.id=:definitionPointId"), }) @@ -57,13 +59,17 @@ public class RoleAssignment implements java.io.Serializable { @ManyToOne( cascade = CascadeType.MERGE ) @JoinColumn( nullable=false ) private DvObject definitionPoint; + + @Column(nullable = true) + private String privateUrlToken; public RoleAssignment() {} - public RoleAssignment(DataverseRole aRole, RoleAssignee anAssignee, DvObject aDefinitionPoint) { + public RoleAssignment(DataverseRole aRole, RoleAssignee anAssignee, DvObject aDefinitionPoint, String privateUrlToken) { role = aRole; assigneeIdentifier = anAssignee.getIdentifier(); definitionPoint = aDefinitionPoint; + this.privateUrlToken = privateUrlToken; } public Long getId() { @@ -97,7 +103,11 @@ public DvObject getDefinitionPoint() { public void setDefinitionPoint(DvObject definitionPoint) { this.definitionPoint = definitionPoint; } - + + public String getPrivateUrlToken() { + return privateUrlToken; + } + @Override public int hashCode() { int hash = 7; diff --git a/src/main/java/edu/harvard/iq/dataverse/RolePermissionFragment.java b/src/main/java/edu/harvard/iq/dataverse/RolePermissionFragment.java index c420c4692f2..89827530b33 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RolePermissionFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/RolePermissionFragment.java @@ -186,7 +186,8 @@ public void assignRole(ActionEvent evt) { private void assignRole(RoleAssignee ra, DataverseRole r) { try { - commandEngine.submit(new AssignRoleCommand(ra, r, dvObject, dvRequestService.getDataverseRequest())); + String privateUrlToken = null; + commandEngine.submit(new AssignRoleCommand(ra, r, dvObject, dvRequestService.getDataverseRequest(), privateUrlToken)); JH.addMessage(FacesMessage.SEVERITY_INFO, "Role " + r.getName() + " assigned to " + ra.getDisplayInfo().getTitle() + " on " + dvObject.getDisplayName()); } catch (CommandException ex) { JH.addMessage(FacesMessage.SEVERITY_ERROR, "Can't assign role: " + ex.getMessage()); 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 169921a5a39..2343a680564 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -20,12 +20,14 @@ import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.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.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.json.JsonParser; @@ -169,6 +171,9 @@ String getWrappedMessageWhenJson() { @EJB protected SavedSearchServiceBean savedSearchSvc; + @EJB + protected PrivateUrlServiceBean privateUrlSvc; + @PersistenceContext(unitName = "VDCNet-ejbPU") protected EntityManager em; @@ -227,9 +232,14 @@ protected String getRequestApiKey() { */ protected User findUserOrDie() throws WrappedResponse { final String requestApiKey = getRequestApiKey(); - return ( requestApiKey == null ) - ? GuestUser.get() - : findAuthenticatedUserOrDie(requestApiKey); + if (requestApiKey == null) { + return GuestUser.get(); + } + PrivateUrlUser privateUrlUser = privateUrlSvc.getPrivateUrlUserFromToken(requestApiKey); + if (privateUrlUser != null) { + return privateUrlUser; + } + return findAuthenticatedUserOrDie(requestApiKey); } /** 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 522a28362f7..cc43e3b2e9c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -20,7 +20,9 @@ import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataaccess.DataFileIO; import edu.harvard.iq.dataverse.dataaccess.DataFileZipper; import edu.harvard.iq.dataverse.dataaccess.FileAccessIO; @@ -945,7 +947,7 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { } } - AuthenticatedUser user = null; + User user = null; /** * Authentication/authorization: @@ -963,7 +965,11 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { if (session.getUser().isAuthenticated()) { user = (AuthenticatedUser) session.getUser(); } else { - logger.fine("User associated with the session is not an authenticated user. (Guest access will be assumed)."); + logger.fine("User associated with the session is not an authenticated user."); + if (session.getUser() instanceof PrivateUrlUser) { + logger.fine("User associated with the session is a PrivateUrlUser user."); + user = session.getUser(); + } if (session.getUser() instanceof GuestUser) { logger.fine("User associated with the session is indeed a guest user."); } @@ -975,13 +981,18 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { logger.fine("Session is null."); } - AuthenticatedUser apiTokenUser = null; + User apiTokenUser = null; if ((apiToken != null)&&(apiToken.length()!=64)) { // We'll also try to obtain the user information from the API token, // if supplied: - apiTokenUser = findUserByApiToken(apiToken); + try { + logger.fine("calling apiTokenUser = findUserOrDie()..."); + apiTokenUser = findUserOrDie(); + } catch (WrappedResponse wr) { + logger.fine("Message from findUserOrDie(): " + wr.getMessage()); + } if (apiTokenUser == null) { logger.warning("API token-based auth: Unable to find a user with the API token provided."); @@ -1000,14 +1011,14 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { if (user != null) { // it's not unthinkable, that a null user (i.e., guest user) could be given // the ViewUnpublished permission! - logger.fine("Session-based auth: user "+user.getName()+" has access rights on the non-restricted, unpublished datafile."); + logger.fine("Session-based auth: user " + user.getIdentifier() + " has access rights on the non-restricted, unpublished datafile."); } return true; } if (apiTokenUser != null) { if (permissionService.userOn(apiTokenUser, df.getOwner()).has(Permission.ViewUnpublishedDataset)) { - logger.fine("Session-based auth: user "+apiTokenUser.getName()+" has access rights on the non-restricted, unpublished datafile."); + logger.fine("Session-based auth: user " + apiTokenUser.getIdentifier() + " has access rights on the non-restricted, unpublished datafile."); return true; } } @@ -1036,12 +1047,12 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { if (published) { if (hasAccessToRestrictedBySession) { if (user != null) { - logger.fine("Session-based auth: user "+user.getName()+" is granted access to the restricted, published datafile."); + logger.fine("Session-based auth: user " + user.getIdentifier() + " is granted access to the restricted, published datafile."); } else { logger.fine("Session-based auth: guest user is granted access to the restricted, published datafile."); } } else { - logger.fine("Token-based auth: user "+apiTokenUser.getName()+" is granted access to the restricted, published datafile."); + logger.fine("Token-based auth: user " + apiTokenUser.getIdentifier() + " is granted access to the restricted, published datafile."); } return true; } else { @@ -1054,7 +1065,7 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { if (hasAccessToRestrictedBySession) { if (permissionService.on(df.getOwner()).has(Permission.ViewUnpublishedDataset)) { if (user != null) { - logger.fine("Session-based auth: user " + user.getName() + " is granted access to the restricted, unpublished datafile."); + logger.fine("Session-based auth: user " + user.getIdentifier() + " is granted access to the restricted, unpublished datafile."); } else { logger.fine("Session-based auth: guest user is granted access to the restricted, unpublished datafile."); } @@ -1062,7 +1073,7 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { } } else { if (apiTokenUser != null && permissionService.userOn(apiTokenUser, df.getOwner()).has(Permission.ViewUnpublishedDataset)) { - logger.fine("Token-based auth: user " + apiTokenUser.getName() + " is granted access to the restricted, unpublished datafile."); + logger.fine("Token-based auth: user " + apiTokenUser.getIdentifier() + " is granted access to the restricted, unpublished datafile."); } } } @@ -1095,7 +1106,12 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { // Will try to obtain the user information from the API token, // if supplied: - user = findUserByApiToken(apiToken); + try { + logger.fine("calling user = findUserOrDie()..."); + user = findUserOrDie(); + } catch (WrappedResponse wr) { + logger.fine("Message from findUserOrDie(): " + wr.getMessage()); + } if (user == null) { logger.warning("API token-based auth: Unable to find a user with the API token provided."); @@ -1104,32 +1120,32 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { if (permissionService.userOn(user, df).has(Permission.DownloadFile)) { if (published) { - logger.fine("API token-based auth: User "+user.getName()+" has rights to access the datafile."); + logger.fine("API token-based auth: User " + user.getIdentifier() + " has rights to access the datafile."); return true; } else { // if the file is NOT published, we will let them download the // file ONLY if they also have the permission to view // unpublished verions: if (permissionService.userOn(user, df.getOwner()).has(Permission.ViewUnpublishedDataset)) { - logger.fine("API token-based auth: User "+user.getName()+" has rights to access the (unpublished) datafile."); + logger.fine("API token-based auth: User " + user.getIdentifier() + " has rights to access the (unpublished) datafile."); return true; } else { - logger.fine("API token-based auth: User "+user.getName()+" is not authorized to access the (unpublished) datafile."); + logger.fine("API token-based auth: User " + user.getIdentifier() + " is not authorized to access the (unpublished) datafile."); } } } else { - logger.fine("API token-based auth: User "+user.getName()+" is not authorized to access the datafile."); + logger.fine("API token-based auth: User " + user.getIdentifier() + " is not authorized to access the datafile."); } return false; } if (user != null) { - logger.fine("Session-based auth: user " + user.getName() + " has NO access rights on the requested datafile."); + logger.fine("Session-based auth: user " + user.getIdentifier() + " has NO access rights on the requested datafile."); } if (apiTokenUser != null) { - logger.fine("Token-based auth: user " + apiTokenUser.getName() + " has NO access rights on the requested datafile."); + logger.fine("Token-based auth: user " + apiTokenUser.getIdentifier() + " has NO access rights on the requested datafile."); } if (user == null && apiTokenUser == 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 7f5f8a9533c..82c4dcdea9d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -9,7 +9,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.MetadataBlock; -import static edu.harvard.iq.dataverse.api.AbstractApiBean.errorResponse; +import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.users.User; @@ -17,14 +17,18 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.DeletePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.DestroyDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetPrivateUrlCommand; +import edu.harvard.iq.dataverse.engine.command.impl.ListRoleAssignments; import edu.harvard.iq.dataverse.engine.command.impl.ListVersionsCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.SetDatasetCitationDateCommand; @@ -32,6 +36,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ddi.DdiExportUtil; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JsonParseException; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; @@ -537,12 +542,71 @@ public Response createAssignment(String userOrGroup, @PathParam("identifier") St return errorResponse(Response.Status.BAD_REQUEST, "Assignee not found"); } DataverseRole theRole = rolesSvc.findBuiltinRoleByAlias("admin"); + String privateUrlToken = null; return okResponse( - json(execCommand(new AssignRoleCommand(assignee, theRole, dataset, createDataverseRequest(findUserOrDie()))))); + json(execCommand(new AssignRoleCommand(assignee, theRole, dataset, createDataverseRequest(findUserOrDie()), privateUrlToken)))); } catch (WrappedResponse ex) { LOGGER.log(Level.WARNING, "Can''t create assignment: {0}", ex.getMessage()); return ex.getResponse(); } } + @GET + @Path("{identifier}/assignments") + public Response getAssignments(@PathParam("identifier") String id) { + try { + JsonArrayBuilder jab = Json.createArrayBuilder(); + for (RoleAssignment ra : execCommand(new ListRoleAssignments(createDataverseRequest(findUserOrDie()), findDatasetOrDie(id)))) { + jab.add(json(ra)); + } + return okResponse(jab); + } catch (WrappedResponse ex) { + LOGGER.log(Level.WARNING, "Can't list assignments: {0}", ex.getMessage()); + return ex.getResponse(); + } + } + + @GET + @Path("{id}/privateUrl") + public Response getPrivateUrlData(@PathParam("id") String idSupplied) { + try { + PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(createDataverseRequest(findUserOrDie()), findDatasetOrDie(idSupplied))); + if (privateUrl != null) { + return okResponse(json(privateUrl)); + } else { + return errorResponse(Response.Status.NOT_FOUND, "Private URL not found."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @POST + @Path("{id}/privateUrl") + public Response createPrivateUrl(@PathParam("id") String idSupplied) { + try { + return okResponse(json(execCommand(new CreatePrivateUrlCommand(createDataverseRequest(findUserOrDie()), findDatasetOrDie(idSupplied))))); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @DELETE + @Path("{id}/privateUrl") + public Response deletePrivateUrl(@PathParam("id") String idSupplied) { + try { + User user = findUserOrDie(); + Dataset dataset = findDatasetOrDie(idSupplied); + PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(createDataverseRequest(user), dataset)); + if (privateUrl != null) { + execCommand(new DeletePrivateUrlCommand(createDataverseRequest(user), dataset)); + return okResponse("Private URL deleted."); + } else { + return errorResponse(Response.Status.NOT_FOUND, "No Private URL to delete."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + } 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 a787f3e26ee..d0e60d7fd04 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -462,10 +462,11 @@ public Response createAssignment( RoleAssignmentDTO ra, @PathParam("identifier") if ( theRole == null ) { return errorResponse( Status.BAD_REQUEST, "Can't find role named '" + ra.getRole() + "' in dataverse " + dataverse); } + String privateUrlToken = null; return okResponse( json( - execCommand( new AssignRoleCommand(assignee, theRole, dataverse, createDataverseRequest(findUserOrDie()))))); + execCommand(new AssignRoleCommand(assignee, theRole, dataverse, createDataverseRequest(findUserOrDie()), privateUrlToken)))); } catch (WrappedResponse ex) { LOGGER.log(Level.WARNING, "Can''t create assignment: {0}", ex.getMessage()); 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 cbb5a7149ac..357280aae7c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/DataverseRole.java @@ -56,6 +56,7 @@ public class DataverseRole implements Serializable { public static final String EDITOR = "editor"; public static final String MANAGER = "manager"; public static final String CURATOR = "curator"; + public static final String MEMBER = "member"; public static final Comparator CMP_BY_NAME = new Comparator(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java new file mode 100644 index 00000000000..147a61bac7c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java @@ -0,0 +1,64 @@ +package edu.harvard.iq.dataverse.authorization.users; + +import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; +import edu.harvard.iq.dataverse.util.BundleUtil; + +/** + * A PrivateUrlUser is virtual in the sense that it does not have a row in the + * authenticateduser table. It exists so when a Private URL is enabled for a + * dataset, we can assign a read-only role ("member") to the identifier for the + * PrivateUrlUser. (We will make no attempt to internationalize the identifier, + * which is stored in the roleassignment table.) + */ +public class PrivateUrlUser implements User { + + public static final String PREFIX = ":privateUrl"; + + /** + * In the future, this could probably be dvObjectId rather than datasetId, + * if necessary. It's really just roleAssignment.getDefinitionPoint(), which + * is a DvObject. + */ + private final long datasetId; + + public PrivateUrlUser(long datasetId) { + this.datasetId = datasetId; + } + + public long getDatasetId() { + return datasetId; + } + + /** + * @return By always returning false for isAuthenticated(), we prevent a + * name from appearing in the corner as well as preventing an account page + * and MyData from being accessible. The user can still navigate to the home + * page but can only see published datasets. + */ + @Override + public boolean isAuthenticated() { + return false; + } + + @Override + public boolean isBuiltInUser() { + return false; + } + + @Override + public boolean isSuperuser() { + return false; + } + + @Override + public String getIdentifier() { + return PREFIX + datasetId; + } + + @Override + public RoleAssigneeDisplayInfo getDisplayInfo() { + String title = BundleUtil.getStringFromBundle("dataset.privateurl.roleassigeeTitle"); + return new RoleAssigneeDisplayInfo(title, null); + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java index 2bf5e719c5f..d08d51d906f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java @@ -25,9 +25,11 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; import javax.persistence.EntityManager; /** @@ -95,4 +97,8 @@ public interface CommandContext { public UserNotificationServiceBean notifications(); public AuthenticationServiceBean authentication(); + + public SystemConfig systemConfig(); + + public PrivateUrlServiceBean privateUrl(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java index e02eb7d01be..767bee92619 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java @@ -29,25 +29,28 @@ public class AssignRoleCommand extends AbstractCommand { private final DataverseRole role; private final RoleAssignee grantee; private final DvObject defPoint; + private final String privateUrlToken; /** * @param anAssignee The user being granted the role * @param aRole the role being granted to the user * @param assignmentPoint the dataverse on which the role is granted. * @param aRequest + * @param privateUrlToken An optional token used by the Private Url feature. */ - public AssignRoleCommand(RoleAssignee anAssignee, DataverseRole aRole, DvObject assignmentPoint, DataverseRequest aRequest) { + public AssignRoleCommand(RoleAssignee anAssignee, DataverseRole aRole, DvObject assignmentPoint, DataverseRequest aRequest, String privateUrlToken) { // for data file check permission on owning dataset super(aRequest, assignmentPoint instanceof DataFile ? assignmentPoint.getOwner() : assignmentPoint); role = aRole; grantee = anAssignee; defPoint = assignmentPoint; + this.privateUrlToken = privateUrlToken; } @Override public RoleAssignment execute(CommandContext ctxt) throws CommandException { // TODO make sure the role is defined on the dataverse. - RoleAssignment roleAssignment = new RoleAssignment(role, grantee, defPoint); + RoleAssignment roleAssignment = new RoleAssignment(role, grantee, defPoint, privateUrlToken); return ctxt.roles().save(roleAssignment); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetCommand.java index 8684338bc7b..8f99e97d64b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetCommand.java @@ -175,7 +175,8 @@ public Dataset execute(CommandContext ctxt) throws CommandException { logger.log(Level.FINE, "after db update {0}", formatter.format(new Date().getTime())); // set the role to be default contributor role for its dataverse if (importType==null || importType.equals(ImportType.NEW)) { - ctxt.roles().save(new RoleAssignment(savedDataset.getOwner().getDefaultContributorRole(), getRequest().getUser(), savedDataset)); + String privateUrlToken = null; + ctxt.roles().save(new RoleAssignment(savedDataset.getOwner().getDefaultContributorRole(), getRequest().getUser(), savedDataset, privateUrlToken)); } savedDataset.setPermissionModificationTime(new Timestamp(new Date().getTime())); 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 c3234d2e1a8..612953623c7 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 @@ -86,7 +86,8 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { // Find the built in admin role (currently by alias) DataverseRole adminRole = ctxt.roles().findBuiltinRoleByAlias(DataverseRole.ADMIN); - ctxt.roles().save(new RoleAssignment(adminRole, getRequest().getUser(), managedDv)); + String privateUrlToken = null; + ctxt.roles().save(new RoleAssignment(adminRole, getRequest().getUser(), managedDv, privateUrlToken)); managedDv.setPermissionModificationTime(new Timestamp(new Date().getTime())); managedDv = ctxt.dataverses().save(managedDv); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommand.java new file mode 100644 index 00000000000..cc1adbc984a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommand.java @@ -0,0 +1,68 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +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.IllegalCommandException; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import java.util.UUID; +import java.util.logging.Logger; + +@RequiredPermissions(Permission.ManageDatasetPermissions) +public class CreatePrivateUrlCommand extends AbstractCommand { + + private static final Logger logger = Logger.getLogger(CreatePrivateUrlCommand.class.getCanonicalName()); + + final Dataset dataset; + + public CreatePrivateUrlCommand(DataverseRequest dataverseRequest, Dataset theDataset) { + super(dataverseRequest, theDataset); + dataset = theDataset; + } + + @Override + public PrivateUrl execute(CommandContext ctxt) throws CommandException { + logger.fine("Executing CreatePrivateUrlCommand..."); + if (dataset == null) { + /** + * @todo Internationalize this. + */ + String message = "Can't create Private URL. Dataset is null."; + logger.info(message); + throw new IllegalCommandException(message, this); + } + PrivateUrl existing = ctxt.privateUrl().getPrivateUrlFromDatasetId(dataset.getId()); + if (existing != null) { + /** + * @todo Internationalize this. + */ + String message = "Private URL already exists for dataset id " + dataset.getId() + "."; + logger.info(message); + throw new IllegalCommandException(message, this); + } + DatasetVersion latestVersion = dataset.getLatestVersion(); + if (!latestVersion.isDraft()) { + /** + * @todo Internationalize this. + */ + String message = "Can't create Private URL because the latest version of dataset id " + dataset.getId() + " is not a draft."; + logger.info(message); + throw new IllegalCommandException(message, this); + } + PrivateUrlUser privateUrlUser = new PrivateUrlUser(dataset.getId()); + DataverseRole memberRole = ctxt.roles().findBuiltinRoleByAlias(DataverseRole.MEMBER); + final String privateUrlToken = UUID.randomUUID().toString(); + RoleAssignment roleAssignment = ctxt.engine().submit(new AssignRoleCommand(privateUrlUser, memberRole, dataset, getRequest(), privateUrlToken)); + PrivateUrl privateUrl = new PrivateUrl(roleAssignment, dataset, ctxt.systemConfig().getDataverseSiteUrl()); + return privateUrl; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetVersionCommand.java index c2bbbefac86..353766a2994 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetVersionCommand.java @@ -11,7 +11,9 @@ 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.privateurl.PrivateUrl; import java.util.Iterator; +import java.util.logging.Logger; /** * @@ -20,6 +22,8 @@ @RequiredPermissions(Permission.DeleteDatasetDraft) public class DeleteDatasetVersionCommand extends AbstractVoidCommand { + private static final Logger logger = Logger.getLogger(DeleteDatasetVersionCommand.class.getCanonicalName()); + private final Dataset doomed; public DeleteDatasetVersionCommand(DataverseRequest aRequest, Dataset dataset) { @@ -64,6 +68,11 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { dvIt.remove(); } } + PrivateUrl privateUrl = ctxt.engine().submit(new GetPrivateUrlCommand(getRequest(), doomed)); + if (privateUrl != null) { + logger.fine("Deleting Private URL for dataset id " + doomed.getId()); + ctxt.engine().submit(new DeletePrivateUrlCommand(getRequest(), doomed)); + } boolean doNormalSolrDocCleanUp = true; ctxt.index().indexDataset(doomed, doNormalSolrDocCleanUp); return; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommand.java new file mode 100644 index 00000000000..4715100ad8f --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommand.java @@ -0,0 +1,50 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +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; +import java.util.List; +import java.util.logging.Logger; + +/** + * @todo If RevokeRoleCommand ever returns anything other than void (a boolean + * perhaps) pass that value upstream. + */ +@RequiredPermissions(Permission.ManageDatasetPermissions) +public class DeletePrivateUrlCommand extends AbstractVoidCommand { + + private static final Logger logger = Logger.getLogger(DeletePrivateUrlCommand.class.getCanonicalName()); + + final Dataset dataset; + + public DeletePrivateUrlCommand(DataverseRequest aRequest, Dataset theDataset) { + super(aRequest, theDataset); + dataset = theDataset; + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { + logger.fine("Executing DeletePrivateUrlCommand...."); + if (dataset == null) { + /** + * @todo Internationalize this. + */ + String message = "Can't delete Private URL. Dataset is null."; + logger.info(message); + throw new IllegalCommandException(message, this); + } + PrivateUrlUser privateUrlUser = new PrivateUrlUser(dataset.getId()); + List roleAssignments = ctxt.roles().directRoleAssignments(privateUrlUser, dataset); + for (RoleAssignment roleAssignment : roleAssignments) { + ctxt.engine().submit(new RevokeRoleCommand(roleAssignment, getRequest())); + } + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommand.java new file mode 100644 index 00000000000..5e698dcf1b9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommand.java @@ -0,0 +1,36 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.authorization.Permission; +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.privateurl.PrivateUrl; +import java.util.logging.Logger; + +@RequiredPermissions(Permission.ManageDatasetPermissions) +public class GetPrivateUrlCommand extends AbstractCommand { + + private static final Logger logger = Logger.getLogger(GetPrivateUrlCommand.class.getCanonicalName()); + + private final Dataset dataset; + + public GetPrivateUrlCommand(DataverseRequest aRequest, Dataset theDataset) { + super(aRequest, theDataset); + dataset = theDataset; + } + + @Override + public PrivateUrl execute(CommandContext ctxt) throws CommandException { + logger.fine("GetPrivateUrlCommand called"); + Long datasetId = dataset.getId(); + if (datasetId == null) { + // Perhaps a dataset is being created in the GUI. + return null; + } + return ctxt.privateUrl().getPrivateUrlFromDatasetId(datasetId); + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListRoleAssignments.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListRoleAssignments.java index b5493b1e024..ed438bc3815 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListRoleAssignments.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListRoleAssignments.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; @@ -14,12 +14,11 @@ * * @author michael */ -//@todo should this command exist for other dvObjects @RequiredPermissions( Permission.ManageDataversePermissions ) public class ListRoleAssignments extends AbstractCommand> { - private final Dataverse definitionPoint; - public ListRoleAssignments(DataverseRequest aRequest, Dataverse aDefinitionPoint) { + private final DvObject definitionPoint; + public ListRoleAssignments(DataverseRequest aRequest, DvObject aDefinitionPoint) { super(aRequest, aDefinitionPoint); definitionPoint = aDefinitionPoint; } 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 36792f20054..0d33fcbbfb6 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 @@ -22,6 +22,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.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.search.IndexResponse; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.io.IOException; @@ -29,6 +30,7 @@ import java.util.Date; import java.util.List; import java.util.ResourceBundle; +import java.util.logging.Logger; /** * @@ -37,6 +39,8 @@ @RequiredPermissions(Permission.PublishDataset) public class PublishDatasetCommand extends AbstractCommand { + private static final Logger logger = Logger.getLogger(PublishDatasetCommand.class.getCanonicalName()); + boolean minorRelease = false; Dataset theDataset; @@ -228,6 +232,12 @@ public Dataset execute(CommandContext ctxt) throws CommandException { } } + PrivateUrl privateUrl = ctxt.engine().submit(new GetPrivateUrlCommand(getRequest(), savedDataset)); + if (privateUrl != null) { + logger.fine("Deleting Private URL for dataset id " + savedDataset.getId()); + ctxt.engine().submit(new DeletePrivateUrlCommand(getRequest(), savedDataset)); + } + /* MoveIndexing to after DOI update so that if command exception is thrown the re-index will not diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrl.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrl.java new file mode 100644 index 00000000000..ff3a14ec72e --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrl.java @@ -0,0 +1,54 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.RoleAssignment; + +/** + * Dataset authors can create and send a Private URL to a reviewer to see the + * lasted draft of their dataset (even if the dataset has never been published) + * without having to create an account. When the dataset is published, the + * Private URL is deleted. + */ +public class PrivateUrl { + + private final Dataset dataset; + private final RoleAssignment roleAssignment; + /** + * The unique string of characters in the Private URL that associates it + * (the link) with a particular dataset. + * + * The token is also available at roleAssignment.getPrivateUrlToken(). + */ + private final String token; + /** + * This is the link that the reviewer will click. + * + * @todo This link should probably be some sort of URL object rather than a + * String. + */ + private final String link; + + public PrivateUrl(RoleAssignment roleAssignment, Dataset dataset, String dataverseSiteUrl) { + this.token = roleAssignment.getPrivateUrlToken(); + this.link = dataverseSiteUrl + "/privateurl.xhtml?token=" + token; + this.dataset = dataset; + this.roleAssignment = roleAssignment; + } + + public Dataset getDataset() { + return dataset; + } + + public RoleAssignment getRoleAssignment() { + return roleAssignment; + } + + public String getToken() { + return token; + } + + public String getLink() { + return link; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlPage.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlPage.java new file mode 100644 index 00000000000..fc28f6ed6a8 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlPage.java @@ -0,0 +1,51 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.DataverseSession; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.view.ViewScoped; +import javax.inject.Inject; +import javax.inject.Named; + +@ViewScoped +@Named("PrivateUrlPage") +public class PrivateUrlPage implements Serializable { + + private static final Logger logger = Logger.getLogger(PrivateUrlPage.class.getCanonicalName()); + + @EJB + PrivateUrlServiceBean privateUrlService; + @Inject + DataverseSession session; + + /** + * The unique string used to look up a PrivateUrlUser and the associated + * draft dataset version to redirect the user to. + */ + String token; + + public String init() { + try { + PrivateUrlRedirectData privateUrlRedirectData = privateUrlService.getPrivateUrlRedirectDataFromToken(token); + String draftDatasetPageToBeRedirectedTo = privateUrlRedirectData.getDraftDatasetPageToBeRedirectedTo() + "&faces-redirect=true"; + PrivateUrlUser privateUrlUser = privateUrlRedirectData.getPrivateUrlUser(); + session.setUser(privateUrlUser); + logger.info("Redirecting PrivateUrlUser '" + privateUrlUser.getIdentifier() + "' to " + draftDatasetPageToBeRedirectedTo); + return draftDatasetPageToBeRedirectedTo; + } catch (Exception ex) { + logger.info("Exception processing Private URL token '" + token + "':" + ex); + return "/404.xhtml"; + } + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlRedirectData.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlRedirectData.java new file mode 100644 index 00000000000..cd891a99c21 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlRedirectData.java @@ -0,0 +1,39 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; + +/** + * PrivateUrlRedirectData is for the person clicking the Private URL link, who + * is often a reviewer. In a browser, we need to set the session to the + * PrivateUrlUser (who has ViewUnpublishedDataset and related permission on the + * dataset and then redirect that user to the draft version of the dataset. + */ +public class PrivateUrlRedirectData { + + private final PrivateUrlUser privateUrlUser; + private final String draftDatasetPageToBeRedirectedTo; + + /** + * @throws java.lang.Exception The reason why a PrivateUrlRedirectData + * object could not be instantiated. + */ + public PrivateUrlRedirectData(PrivateUrlUser privateUrlUser, String draftDatasetPageToBeRedirectedTo) throws Exception { + if (privateUrlUser == null) { + throw new Exception("PrivateUrlUser cannot be null"); + } + if (draftDatasetPageToBeRedirectedTo == null) { + throw new Exception("draftDatasetPageToBeRedirectedTo cannot be null"); + } + this.privateUrlUser = privateUrlUser; + this.draftDatasetPageToBeRedirectedTo = draftDatasetPageToBeRedirectedTo; + } + + public PrivateUrlUser getPrivateUrlUser() { + return privateUrlUser; + } + + public String getDraftDatasetPageToBeRedirectedTo() { + return draftDatasetPageToBeRedirectedTo; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java new file mode 100644 index 00000000000..efe64052c4a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java @@ -0,0 +1,108 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.util.SystemConfig; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.inject.Named; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.NonUniqueResultException; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +/** + * + * PrivateUrlServiceBean depends on Glassfish and Postgres being available and + * it is tested with API tests in DatasetIT. Code that can execute without any + * runtime dependencies should be put in PrivateUrlUtil so it can be unit + * tested. + */ +@Stateless +@Named +public class PrivateUrlServiceBean implements Serializable { + + private static final Logger logger = Logger.getLogger(PrivateUrlServiceBean.class.getCanonicalName()); + + @PersistenceContext(unitName = "VDCNet-ejbPU") + private EntityManager em; + + @EJB + DatasetServiceBean datasetServiceBean; + + @EJB + SystemConfig systemConfig; + + /** + * @return A PrivateUrl if the dataset has one or null. + */ + public PrivateUrl getPrivateUrlFromDatasetId(long datasetId) { + RoleAssignment roleAssignment = getPrivateUrlRoleAssignmentFromDataset(datasetServiceBean.find(datasetId)); + return PrivateUrlUtil.getPrivateUrlFromRoleAssignment(roleAssignment, systemConfig.getDataverseSiteUrl()); + } + + /** + * @return A PrivateUrlUser if one can be found using the token or null. + */ + public PrivateUrlUser getPrivateUrlUserFromToken(String token) { + return PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(getRoleAssignmentFromPrivateUrlToken(token)); + } + + /** + * @return PrivateUrlRedirectData if it can be found using the token or + * null. + */ + public PrivateUrlRedirectData getPrivateUrlRedirectDataFromToken(String token) { + return PrivateUrlUtil.getPrivateUrlRedirectData(getRoleAssignmentFromPrivateUrlToken(token)); + } + + /** + * @return A RoleAssignment or null. + * + * @todo This might be a good place for Optional. + */ + private RoleAssignment getRoleAssignmentFromPrivateUrlToken(String privateUrlToken) { + if (privateUrlToken == null) { + return null; + } + TypedQuery query = em.createNamedQuery( + "RoleAssignment.listByPrivateUrlToken", + RoleAssignment.class); + query.setParameter("privateUrlToken", privateUrlToken); + try { + RoleAssignment roleAssignment = query.getSingleResult(); + return roleAssignment; + } catch (NoResultException | NonUniqueResultException ex) { + return null; + } + } + + /** + * @param dataset A non-null dataset; + * @return A role assignment for a Private URL, if found, or null. + * + * @todo This might be a good place for Optional. + */ + private RoleAssignment getPrivateUrlRoleAssignmentFromDataset(Dataset dataset) { + if (dataset == null) { + return null; + } + TypedQuery query = em.createNamedQuery( + "RoleAssignment.listByAssigneeIdentifier_DefinitionPointId", + RoleAssignment.class); + PrivateUrlUser privateUrlUser = new PrivateUrlUser(dataset.getId()); + query.setParameter("assigneeIdentifier", privateUrlUser.getIdentifier()); + query.setParameter("definitionPointId", dataset.getId()); + try { + return query.getSingleResult(); + } catch (NoResultException | NonUniqueResultException ex) { + return null; + } + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtil.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtil.java new file mode 100644 index 00000000000..15a5bdd1993 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtil.java @@ -0,0 +1,198 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +public class PrivateUrlUtil { + + private static final Logger logger = Logger.getLogger(PrivateUrlUtil.class.getCanonicalName()); + + /** + * Use of this method should be limited to + * RoleAssigneeServiceBean.getRoleAssignee, which is the centralized place + * to return a RoleAssignee (which can be either a User or a Group) when all + * you have is the string that is their identifier. + * + * @todo Consider using a new character, something other than ":" as a + * namespace for PrivateUrlUser rather than ":" which is for a short list of + * unchanging "predefinedRoleAssignees" which consists of + * :authenticated-users, :AllUsers, and :guest. A PrivateUrlUser is + * something of a different animal in that its identifier will vary based on + * the dataset that it is associated with. The number at the end of the + * identifier will vary. + * + * @param identifier The identifier is expected to start with the + * PrivateUrlUser.PREFIX and end with a number for a dataset, + * ":privateUrl42", for example. The ":" indicates that this is a User + * rather than a Group (groups start with "&"). The number at the end of the + * identifier of a PrivateUrlUser is all we have to associate the role + * assignee identifier with a dataset. If we had the role assignment itself + * in our hands, we would simply get the dataset id from + * RoleAssignment.getDefinitionPoint and then use it to instantiate a + * PrivateUrlUser. + * + * @return A valid PrivateUrlUser (which like any User or Group is a + * RoleAssignee) if a valid identifer is provided or null. + */ + public static RoleAssignee identifier2roleAssignee(String identifier) { + String[] parts = identifier.split(PrivateUrlUser.PREFIX); + long datasetId; + try { + datasetId = new Long(parts[1]); + } catch (ArrayIndexOutOfBoundsException | NumberFormatException ex) { + logger.fine("Could not find dataset id in '" + identifier + "': " + ex); + return null; + } + return new PrivateUrlUser(datasetId); + } + + /** + * @todo If there is a use case for this outside the context of Private URL, + * move this method to somewhere more centralized. + */ + static Dataset getDatasetFromRoleAssignment(RoleAssignment roleAssignment) { + if (roleAssignment == null) { + return null; + } + DvObject dvObject = roleAssignment.getDefinitionPoint(); + if (dvObject == null) { + return null; + } + if (dvObject instanceof Dataset) { + return (Dataset) roleAssignment.getDefinitionPoint(); + } else { + return null; + } + } + + /** + * @return DatasetVersion if a draft or null. + * + * @todo If there is a use case for this outside the context of Private URL, + * move this method to somewhere more centralized. + */ + static public DatasetVersion getDraftDatasetVersionFromRoleAssignment(RoleAssignment roleAssignment) { + if (roleAssignment == null) { + return null; + } + Dataset dataset = getDatasetFromRoleAssignment(roleAssignment); + if (dataset != null) { + DatasetVersion latestVersion = dataset.getLatestVersion(); + if (latestVersion.isDraft()) { + return latestVersion; + } + } + logger.fine("Couldn't find draft, returning null"); + return null; + } + + static public PrivateUrlUser getPrivateUrlUserFromRoleAssignment(RoleAssignment roleAssignment) { + if (roleAssignment == null) { + return null; + } + Dataset dataset = getDatasetFromRoleAssignment(roleAssignment); + if (dataset != null) { + PrivateUrlUser privateUrlUser = new PrivateUrlUser(dataset.getId()); + return privateUrlUser; + } + return null; + } + + /** + * @return PrivateUrlRedirectData or null. + * + * @todo Show the Exception to the user? + */ + public static PrivateUrlRedirectData getPrivateUrlRedirectData(RoleAssignment roleAssignment) { + PrivateUrlUser privateUrlUser = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(roleAssignment); + String draftDatasetPageToBeRedirectedTo = PrivateUrlUtil.getDraftDatasetPageToBeRedirectedTo(roleAssignment); + try { + return new PrivateUrlRedirectData(privateUrlUser, draftDatasetPageToBeRedirectedTo); + } catch (Exception ex) { + logger.info("Exception caught trying to instantiate PrivateUrlRedirectData: " + ex); + return null; + } + } + + /** + * Returns a relative URL or "UNKNOWN." + */ + static String getDraftDatasetPageToBeRedirectedTo(RoleAssignment roleAssignment) { + DatasetVersion datasetVersion = getDraftDatasetVersionFromRoleAssignment(roleAssignment); + return getDraftUrl(datasetVersion); + } + + /** + * Returns a relative URL or "UNKNOWN." + */ + static String getDraftUrl(DatasetVersion draft) { + if (draft != null) { + Dataset dataset = draft.getDataset(); + if (dataset != null) { + String persistentId = dataset.getGlobalId(); + /** + * @todo Investigate why dataset.getGlobalId() yields the String + * "null:null/null" when I expect null value. This smells like a + * bug. + */ + if (!"null:null/null".equals(persistentId)) { + String relativeUrl = "/dataset.xhtml?persistentId=" + persistentId + "&version=DRAFT"; + return relativeUrl; + } + } + } + return "UNKNOWN"; + } + + static PrivateUrl getPrivateUrlFromRoleAssignment(RoleAssignment roleAssignment, String dataverseSiteUrl) { + if (dataverseSiteUrl == null) { + logger.info("dataverseSiteUrl was null. Can not instantiate a PrivateUrl object."); + return null; + } + Dataset dataset = PrivateUrlUtil.getDatasetFromRoleAssignment(roleAssignment); + if (dataset != null) { + PrivateUrl privateUrl = new PrivateUrl(roleAssignment, dataset, dataverseSiteUrl); + return privateUrl; + } else { + return null; + } + } + + static PrivateUrlUser getPrivateUrlUserFromRoleAssignment(RoleAssignment roleAssignment, RoleAssignee roleAssignee) { + if (roleAssignment != null) { + if (roleAssignee instanceof PrivateUrlUser) { + return (PrivateUrlUser) roleAssignee; + } + } + return null; + } + + /** + * @return A list of the CamelCase "names" of required permissions, not the + * human-readable equivalents. + * + * @todo Move this to somewhere more central. + */ + public static List getRequiredPermissions(CommandException ex) { + List stringsToReturn = new ArrayList<>(); + Map> map = ex.getFailedCommand().getRequiredPermissions(); + map.entrySet().stream().forEach((entry) -> { + entry.getValue().stream().forEach((permission) -> { + stringsToReturn.add(permission.name()); + }); + }); + return stringsToReturn; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 94a28859f18..0d254ad068c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -14,6 +14,7 @@ import edu.harvard.iq.dataverse.authorization.groups.Group; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.JsfHelper; @@ -748,6 +749,10 @@ private String getPermissionFilterQuery(User user, SolrQuery solrQuery, Datavers // initialize to public only to be safe String dangerZoneNoSolrJoin = null; + if (user instanceof PrivateUrlUser) { + user = GuestUser.get(); + } + // ---------------------------------------------------- // (1) Is this a GuestUser? // Yes, all set, give back "publicOnly" filter string diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 57d9e7d8d5e..ccd56173edf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -27,6 +27,7 @@ import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.util.DatasetFieldWalker; import java.util.Set; import javax.json.Json; @@ -88,6 +89,7 @@ public static JsonObjectBuilder json( RoleAssignment ra ) { .add("assignee", ra.getAssigneeIdentifier() ) .add("roleId", ra.getRole().getId() ) .add("_roleAlias", ra.getRole().getAlias()) + .add("privateUrlToken", ra.getPrivateUrlToken()) .add("definitionPointId", ra.getDefinitionPoint().getId() ); } @@ -472,7 +474,15 @@ public static JsonObjectBuilder json( AuthenticationProviderRow aRow ) { .add("enabled", aRow.isEnabled()) ; } - + + public static JsonObjectBuilder json(PrivateUrl privateUrl) { + return jsonObjectBuilder() + // We provide the token here as a convenience even though it is also in the role assignment. + .add("token", privateUrl.getToken()) + .add("link", privateUrl.getLink()) + .add("roleAssignment", json(privateUrl.getRoleAssignment())); + } + public static JsonObjectBuilder json(T j ) { if (j instanceof ExplicitGroup) { ExplicitGroup eg = (ExplicitGroup) j; diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 0642a52fe21..228f7984657 100755 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -85,6 +85,11 @@ +
  • + + + +
  • @@ -664,6 +669,38 @@ + + + +

    #{bundle['dataset.privateurl.tip']}

    +
    +
    +

    #{bundle['dataset.privateurl.absent']}

    +
    + +
    +
    + + + +
    +
    + + +

    #{bundle['dataset.privateurl.cannotCreate']}

    +
    + +
    +
    +
    + +

    #{bundle['dataset.privateurl.disableConfirmationText']}

    + + +

    #{bundle['file.deleteFileDialog.tip']}

    diff --git a/src/main/webapp/privateurl.xhtml b/src/main/webapp/privateurl.xhtml new file mode 100644 index 00000000000..e2319053f99 --- /dev/null +++ b/src/main/webapp/privateurl.xhtml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 7af230e1275..c878882b0b8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1,21 +1,32 @@ package edu.harvard.iq.dataverse.api; import com.jayway.restassured.RestAssured; +import static com.jayway.restassured.RestAssured.given; import com.jayway.restassured.response.Response; import java.util.logging.Logger; import org.junit.BeforeClass; import org.junit.Test; -import org.junit.AfterClass; -import static com.jayway.restassured.RestAssured.given; +import com.jayway.restassured.path.json.JsonPath; + +import java.util.List; +import java.util.Map; +import javax.json.JsonObject; +import static javax.ws.rs.core.Response.Status.CREATED; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static javax.ws.rs.core.Response.Status.OK; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static com.jayway.restassured.path.json.JsonPath.with; +import static edu.harvard.iq.dataverse.api.UtilIT.API_TOKEN_HTTP_HEADER; +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import java.util.UUID; import static junit.framework.Assert.assertEquals; public class DatasetsIT { private static final Logger logger = Logger.getLogger(DatasetsIT.class.getCanonicalName()); - private static String username1; - private static String apiToken1; - private static String dataverseAlias1; - private static Integer datasetId1; @BeforeClass public static void setUpClass() { @@ -25,18 +36,71 @@ public static void setUpClass() { @Test public void testCreateDataset() { - Response createUser1 = UtilIT.createRandomUser(); -// createUser1.prettyPrint(); - username1 = UtilIT.getUsernameFromResponse(createUser1); - apiToken1 = UtilIT.getApiTokenFromResponse(createUser1); + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); + + Response deleteDatasetResponse = UtilIT.deleteDatasetViaNativeApi(datasetId, apiToken); + deleteDatasetResponse.prettyPrint(); + assertEquals(200, deleteDatasetResponse.getStatusCode()); - Response createDataverse1Response = UtilIT.createRandomDataverse(apiToken1); - createDataverse1Response.prettyPrint(); - dataverseAlias1 = UtilIT.getAliasFromResponse(createDataverse1Response); + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + deleteDataverseResponse.prettyPrint(); + assertEquals(200, deleteDataverseResponse.getStatusCode()); - Response createDataset1Response = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias1, apiToken1); - createDataset1Response.prettyPrint(); - datasetId1 = UtilIT.getDatasetIdFromResponse(createDataset1Response); + Response deleteUserResponse = UtilIT.deleteUser(username); + deleteUserResponse.prettyPrint(); + assertEquals(200, deleteUserResponse.getStatusCode()); + + } + + /** + * This test requires the root dataverse to be published to pass. + */ + @Test + public void testCreatePublishDestroyDataset() { + + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + assertEquals(200, createUser.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + Response makeSuperUser = UtilIT.makeSuperUser(username); + assertEquals(200, makeSuperUser.getStatusCode()); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + + Response publishDataverse = UtilIT.publishDataverseViaSword(dataverseAlias, apiToken); + assertEquals(200, publishDataverse.getStatusCode()); + Response publishDataset = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); + assertEquals(200, publishDataset.getStatusCode()); + + Response deleteDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + deleteDatasetResponse.prettyPrint(); + assertEquals(200, deleteDatasetResponse.getStatusCode()); + + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + deleteDataverseResponse.prettyPrint(); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + deleteUserResponse.prettyPrint(); + assertEquals(200, deleteUserResponse.getStatusCode()); } @@ -67,26 +131,256 @@ private Response getDatasetAsDdiDto(String persistentIdentifier, String apiToken return response; } - @AfterClass - public static void tearDownClass() { - boolean disabled = false; + /** + * This test requires the root dataverse to be published to pass. + */ + @Test + public void testPrivateUrl() { + + Response createUser = UtilIT.createRandomUser(); +// createUser.prettyPrint(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); - if (disabled) { - return; - } + Response failToCreateWhenDatasetIdNotFound = UtilIT.privateUrlCreate(Integer.MAX_VALUE, apiToken); + failToCreateWhenDatasetIdNotFound.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), failToCreateWhenDatasetIdNotFound.getStatusCode()); - Response deleteDatasetResponse = UtilIT.deleteDatasetViaNativeApi(datasetId1, apiToken1); - deleteDatasetResponse.prettyPrint(); - assertEquals(200, deleteDatasetResponse.getStatusCode()); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + System.out.println("dataset id: " + datasetId); + + Response createContributorResponse = UtilIT.createRandomUser(); + String contributorUsername = UtilIT.getUsernameFromResponse(createContributorResponse); + String contributorApiToken = UtilIT.getApiTokenFromResponse(createContributorResponse); + UtilIT.getRoleAssignmentsOnDataverse(dataverseAlias, apiToken).prettyPrint(); + /** + * dsContributor only has AddDataset per + * scripts/api/data/role-dsContributor.json + */ + Response grantRole = UtilIT.grantRoleOnDataverse(dataverseAlias, DataverseRole.DS_CONTRIBUTOR.toString(), contributorUsername, apiToken); + grantRole.prettyPrint(); + assertEquals(OK.getStatusCode(), grantRole.getStatusCode()); + UtilIT.getRoleAssignmentsOnDataverse(dataverseAlias, apiToken).prettyPrint(); + Response contributorDoesNotHavePermissionToCreatePrivateUrl = UtilIT.privateUrlCreate(datasetId, contributorApiToken); + contributorDoesNotHavePermissionToCreatePrivateUrl.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), contributorDoesNotHavePermissionToCreatePrivateUrl.getStatusCode()); + + Response getDatasetJson = UtilIT.nativeGet(datasetId, apiToken); + getDatasetJson.prettyPrint(); + String protocol1 = JsonPath.from(getDatasetJson.getBody().asString()).getString("data.protocol"); + String authority1 = JsonPath.from(getDatasetJson.getBody().asString()).getString("data.authority"); + String identifier1 = JsonPath.from(getDatasetJson.getBody().asString()).getString("data.identifier"); + String dataset1PersistentId = protocol1 + ":" + authority1 + "/" + identifier1; + + Response uploadFileResponse = UtilIT.uploadRandomFile(dataset1PersistentId, apiToken); + uploadFileResponse.prettyPrint(); + assertEquals(CREATED.getStatusCode(), uploadFileResponse.getStatusCode()); + + Response badApiKeyEmptyString = UtilIT.privateUrlGet(datasetId, ""); + badApiKeyEmptyString.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), badApiKeyEmptyString.getStatusCode()); + Response badApiKeyDoesNotExist = UtilIT.privateUrlGet(datasetId, "junk"); + badApiKeyDoesNotExist.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), badApiKeyDoesNotExist.getStatusCode()); + Response badDatasetId = UtilIT.privateUrlGet(Integer.MAX_VALUE, apiToken); + badDatasetId.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), badDatasetId.getStatusCode()); + Response pristine = UtilIT.privateUrlGet(datasetId, apiToken); + pristine.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), pristine.getStatusCode()); + + Response createPrivateUrl = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrl.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrl.getStatusCode()); + + Response userWithNoRoles = UtilIT.createRandomUser(); + String userWithNoRolesApiToken = UtilIT.getApiTokenFromResponse(userWithNoRoles); + Response unAuth = UtilIT.privateUrlGet(datasetId, userWithNoRolesApiToken); + unAuth.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), unAuth.getStatusCode()); + Response shouldExist = UtilIT.privateUrlGet(datasetId, apiToken); + shouldExist.prettyPrint(); + assertEquals(OK.getStatusCode(), shouldExist.getStatusCode()); + + String tokenForPrivateUrlUser = JsonPath.from(shouldExist.body().asString()).getString("data.token"); + logger.info("privateUrlToken: " + tokenForPrivateUrlUser); + + String urlWithToken = JsonPath.from(shouldExist.body().asString()).getString("data.link"); + logger.info("URL with token: " + urlWithToken); + + assertEquals(tokenForPrivateUrlUser, urlWithToken.substring(urlWithToken.length() - UUID.randomUUID().toString().length())); + + Response getDatasetAsUserWhoClicksPrivateUrl = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get(urlWithToken); + String title = getDatasetAsUserWhoClicksPrivateUrl.getBody().htmlPath().getString("html.head.title"); + assertEquals("Darwin's Finches - " + dataverseAlias + " Dataverse", title); + assertEquals(OK.getStatusCode(), getDatasetAsUserWhoClicksPrivateUrl.getStatusCode()); + + Response junkPrivateUrlToken = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/privateurl.xhtml?token=" + "junk"); + assertEquals("404 Not Found", junkPrivateUrlToken.getBody().htmlPath().getString("html.head.title").substring(0, 13)); + + long roleAssignmentIdFromCreate = JsonPath.from(createPrivateUrl.body().asString()).getLong("data.roleAssignment.id"); + logger.info("roleAssignmentIdFromCreate: " + roleAssignmentIdFromCreate); + + Response badAnonLinkTokenEmptyString = UtilIT.nativeGet(datasetId, ""); + badAnonLinkTokenEmptyString.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), badAnonLinkTokenEmptyString.getStatusCode()); + + Response getWithPrivateUrlToken = UtilIT.nativeGet(datasetId, tokenForPrivateUrlUser); + assertEquals(OK.getStatusCode(), getWithPrivateUrlToken.getStatusCode()); +// getWithPrivateUrlToken.prettyPrint(); + logger.info("http://localhost:8080/privateurl.xhtml?token=" + tokenForPrivateUrlUser); + Response swordStatement = UtilIT.getSwordStatement(dataset1PersistentId, apiToken); + assertEquals(OK.getStatusCode(), swordStatement.getStatusCode()); + Integer fileId = UtilIT.getFileIdFromSwordStatementResponse(swordStatement); + Response downloadFile = UtilIT.downloadFile(fileId, tokenForPrivateUrlUser); + assertEquals(OK.getStatusCode(), downloadFile.getStatusCode()); + Response downloadFileBadToken = UtilIT.downloadFile(fileId, "junk"); + assertEquals(FORBIDDEN.getStatusCode(), downloadFileBadToken.getStatusCode()); + Response notPermittedToListRoleAssignment = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, userWithNoRolesApiToken); + assertEquals(UNAUTHORIZED.getStatusCode(), notPermittedToListRoleAssignment.getStatusCode()); + Response roleAssignments = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, apiToken); + roleAssignments.prettyPrint(); + assertEquals(OK.getStatusCode(), roleAssignments.getStatusCode()); + List assignments = with(roleAssignments.body().asString()).param("member", "member").getJsonObject("data.findAll { data -> data._roleAlias == member }"); + assertEquals(1, assignments.size()); + PrivateUrlUser privateUrlUser = new PrivateUrlUser(datasetId); + assertEquals("Private URL Enabled", privateUrlUser.getDisplayInfo().getTitle()); + List assigneeShouldExistForPrivateUrlUser = with(roleAssignments.body().asString()).param("assigneeString", privateUrlUser.getIdentifier()).getJsonObject("data.findAll { data -> data.assignee == assigneeString }"); + logger.info(assigneeShouldExistForPrivateUrlUser + " found for " + privateUrlUser.getIdentifier()); + assertEquals(1, assigneeShouldExistForPrivateUrlUser.size()); + Map roleAssignment = assignments.get(0); + int roleAssignmentId = (int) roleAssignment.get("id"); + logger.info("role assignment id: " + roleAssignmentId); + assertEquals(roleAssignmentIdFromCreate, roleAssignmentId); + Response revoke = UtilIT.revokeRole(dataverseAlias, roleAssignmentId, apiToken); + revoke.prettyPrint(); + assertEquals(OK.getStatusCode(), revoke.getStatusCode()); + + Response shouldNoLongerExist = UtilIT.privateUrlGet(datasetId, apiToken); + shouldNoLongerExist.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), shouldNoLongerExist.getStatusCode()); + + Response createPrivateUrlUnauth = UtilIT.privateUrlCreate(datasetId, userWithNoRolesApiToken); + createPrivateUrlUnauth.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), createPrivateUrlUnauth.getStatusCode()); + + Response createPrivateUrlAgain = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrlAgain.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrlAgain.getStatusCode()); + + Response shouldNotDeletePrivateUrl = UtilIT.privateUrlDelete(datasetId, userWithNoRolesApiToken); + shouldNotDeletePrivateUrl.prettyPrint(); + assertEquals(UNAUTHORIZED.getStatusCode(), shouldNotDeletePrivateUrl.getStatusCode()); + + Response deletePrivateUrlResponse = UtilIT.privateUrlDelete(datasetId, apiToken); + deletePrivateUrlResponse.prettyPrint(); + assertEquals(OK.getStatusCode(), deletePrivateUrlResponse.getStatusCode()); + + Response tryToDeleteAlreadyDeletedPrivateUrl = UtilIT.privateUrlDelete(datasetId, apiToken); + tryToDeleteAlreadyDeletedPrivateUrl.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), tryToDeleteAlreadyDeletedPrivateUrl.getStatusCode()); + + Response createPrivateUrlOnceAgain = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrlOnceAgain.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrlOnceAgain.getStatusCode()); + + Response tryToCreatePrivateUrlWhenExisting = UtilIT.privateUrlCreate(datasetId, apiToken); + tryToCreatePrivateUrlWhenExisting.prettyPrint(); + assertEquals(BAD_REQUEST.getStatusCode(), tryToCreatePrivateUrlWhenExisting.getStatusCode()); + + Response publishDataverse = UtilIT.publishDataverseViaSword(dataverseAlias, apiToken); + assertEquals(OK.getStatusCode(), publishDataverse.getStatusCode()); + Response publishDataset = UtilIT.publishDatasetViaSword(dataset1PersistentId, apiToken); + assertEquals(OK.getStatusCode(), publishDataset.getStatusCode()); + Response privateUrlTokenShouldBeDeletedOnPublish = UtilIT.privateUrlGet(datasetId, apiToken); + privateUrlTokenShouldBeDeletedOnPublish.prettyPrint(); + assertEquals(NOT_FOUND.getStatusCode(), privateUrlTokenShouldBeDeletedOnPublish.getStatusCode()); + + Response getRoleAssignmentsOnDatasetShouldFailUnauthorized = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, userWithNoRolesApiToken); + assertEquals(UNAUTHORIZED.getStatusCode(), getRoleAssignmentsOnDatasetShouldFailUnauthorized.getStatusCode()); + Response publishingShouldHaveRemovedRoleAssignmentForPrivateUrlUser = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, apiToken); + publishingShouldHaveRemovedRoleAssignmentForPrivateUrlUser.prettyPrint(); + List noAssignmentsForPrivateUrlUser = with(publishingShouldHaveRemovedRoleAssignmentForPrivateUrlUser.body().asString()).param("member", "member").getJsonObject("data.findAll { data -> data._roleAlias == member }"); + assertEquals(0, noAssignmentsForPrivateUrlUser.size()); + + Response tryToCreatePrivateUrlToPublishedVersion = UtilIT.privateUrlCreate(datasetId, apiToken); + tryToCreatePrivateUrlToPublishedVersion.prettyPrint(); + assertEquals(BAD_REQUEST.getStatusCode(), tryToCreatePrivateUrlToPublishedVersion.getStatusCode()); + + String newTitle = "I am changing the title"; + Response updatedMetadataResponse = UtilIT.updateDatasetTitleViaSword(dataset1PersistentId, newTitle, apiToken); + updatedMetadataResponse.prettyPrint(); + assertEquals(OK.getStatusCode(), updatedMetadataResponse.getStatusCode()); + + Response createPrivateUrlForPostVersionOneDraft = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrlForPostVersionOneDraft.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrlForPostVersionOneDraft.getStatusCode()); + + /** + * @todo Make this a more explicit delete of the draft version rather + * than the latest version which we happen to know is a draft. + */ + Response deleteDraftVersion = UtilIT.deleteLatestDatasetVersionViaSwordApi(dataset1PersistentId, apiToken); + deleteDraftVersion.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrlForPostVersionOneDraft.getStatusCode()); + + Response privateUrlRoleAssignmentShouldBeGoneAfterDraftDeleted = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, apiToken); + privateUrlRoleAssignmentShouldBeGoneAfterDraftDeleted.prettyPrint(); + assertEquals(false, privateUrlRoleAssignmentShouldBeGoneAfterDraftDeleted.body().asString().contains(privateUrlUser.getIdentifier())); + + String newTitleAgain = "I am changing the title again"; + Response draftCreatedAgainPostPub = UtilIT.updateDatasetTitleViaSword(dataset1PersistentId, newTitleAgain, apiToken); + draftCreatedAgainPostPub.prettyPrint(); + assertEquals(OK.getStatusCode(), draftCreatedAgainPostPub.getStatusCode()); + + /** + * Making sure the Private URL is deleted when a dataset is destroyed is + * less of an issue now that a Private URL is now effectively only a + * specialized role assignment which is already known to be deleted when + * a dataset is destroy. Still, we'll keep this test in here in case we + * switch Private URL back to being its own table in the future. + */ + Response createPrivateUrlToMakeSureItIsDeletedWithDestructionOfDataset = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrlToMakeSureItIsDeletedWithDestructionOfDataset.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrlToMakeSureItIsDeletedWithDestructionOfDataset.getStatusCode()); + + /** + * @todo What about deaccessioning? We can't test deaccessioning via API + * until https://github.com/IQSS/dataverse/issues/778 is worked on. If + * you deaccession a dataset, is the Private URL deleted? Probably not + * because in order to create a Private URL the dataset version must be + * a draft and for that draft to be deaccessioned it must be published + * first and publishing a version will delete the Private URL. So, we + * shouldn't need to worry about cleaning up Private URLs in the case of + * deaccessioning. + */ + Response makeSuperUser = UtilIT.makeSuperUser(username); + assertEquals(200, makeSuperUser.getStatusCode()); - Response deleteDataverse1Response = UtilIT.deleteDataverse(dataverseAlias1, apiToken1); - deleteDataverse1Response.prettyPrint(); - assertEquals(200, deleteDataverse1Response.getStatusCode()); + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + destroyDatasetResponse.prettyPrint(); + assertEquals(200, destroyDatasetResponse.getStatusCode()); - Response deleteUser1Response = UtilIT.deleteUser(username1); - deleteUser1Response.prettyPrint(); - assertEquals(200, deleteUser1Response.getStatusCode()); + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + deleteDataverseResponse.prettyPrint(); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + Response deleteUserResponse = UtilIT.deleteUser(username); + deleteUserResponse.prettyPrint(); + assertEquals(200, deleteUserResponse.getStatusCode()); + /** + * @todo Should the Search API work with the Private URL token? + */ } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 843cebc2e34..06da2ad4e8d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.api.datadeposit.SwordConfigurationImpl; import com.jayway.restassured.path.xml.XmlPath; import org.junit.Test; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.path.xml.XmlPath.from; import static org.junit.Assert.assertEquals; @@ -363,6 +364,20 @@ static Response publishDatasetViaSword(String persistentId, String apiToken) { .post(swordConfiguration.getBaseUrlPathCurrent() + "/edit/study/" + persistentId); } + static Response publishDatasetViaNativeApi(Integer datasetId, String majorOrMinor, String apiToken) { + /** + * @todo This should be a POST rather than a GET: + * https://github.com/IQSS/dataverse/issues/2431 + * + * @todo Prevent version less than v1.0 to be published (i.e. v0.1): + * https://github.com/IQSS/dataverse/issues/2461 + */ + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .urlEncodingEnabled(false) + .get("/api/datasets/" + datasetId + "/actions/:publish?type=" + majorOrMinor); + } + static Response publishDataverseViaSword(String alias, String apiToken) { return given() .auth().basic(apiToken, EMPTY_STRING) @@ -381,6 +396,100 @@ static Response reindexDataset(String persistentId) { return response; } + static Response nativeGet(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId); + return response; + } + + static Response privateUrlGet(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId + "/privateUrl"); + return response; + } + + static Response privateUrlCreate(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .post("/api/datasets/" + datasetId + "/privateUrl"); + return response; + } + + static Response privateUrlDelete(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/datasets/" + datasetId + "/privateUrl"); + return response; + } + + static Response search(String query, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/search?q=" + query); + } + + static Response indexClear() { + return given() + .get("/api/admin/index/clear"); + } + + static Response index() { + return given() + .get("/api/admin/index"); + } + + static Response enableSetting(SettingsServiceBean.Key settingKey) { + Response response = given().body("true").when().put("/api/admin/settings/" + settingKey); + return response; + } + + static Response deleteSetting(SettingsServiceBean.Key settingKey) { + Response response = given().when().delete("/api/admin/settings/" + settingKey); + return response; + } + + static Response getRoleAssignmentsOnDataverse(String dataverseAliasOrId, String apiToken) { + String url = "/api/dataverses/" + dataverseAliasOrId + "/assignments"; + System.out.println("URL: " + url); + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get(url); + } + + static Response getRoleAssignmentsOnDataset(String datasetId, String persistentId, String apiToken) { + String url = "/api/datasets/" + datasetId + "/assignments"; + System.out.println("URL: " + url); + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get(url); + } + + static Response grantRoleOnDataverse(String definitionPoint, String role, String roleAssignee, String apiToken) { + JsonObjectBuilder roleBuilder = Json.createObjectBuilder(); + roleBuilder.add("assignee", "@" + roleAssignee); + roleBuilder.add("role", role); + JsonObject roleObject = roleBuilder.build(); + logger.info("Granting role on dataverse \"" + definitionPoint + "\": " + role + "... " + roleObject); + return given() + .body(roleObject.toString()).contentType(ContentType.JSON) + .post("api/dataverses/" + definitionPoint + "/assignments?key=" + apiToken); + } + + static Response grantRoleOnDataset(String definitionPoint, String role, String roleAssignee, String apiToken) { + logger.info("Granting role on dataset \"" + definitionPoint + "\": " + role); + return given() + .body("@" + roleAssignee) + .post("api/datasets/" + definitionPoint + "/assignments?key=" + apiToken); + } + + static Response revokeRole(String definitionPoint, long doomed, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("api/dataverses/" + definitionPoint + "/assignments/" + doomed); + } + @Test public void testGetFileIdFromSwordStatementWithNoFiles() { String swordStatementWithNoFiles = "\n" diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java index 10a7edfdf3d..63f32f6ebf8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java @@ -5,11 +5,13 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.search.SearchServiceBean; import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; import javax.persistence.EntityManager; /** @@ -163,5 +165,15 @@ public RoleAssigneeServiceBean roleAssignees() { public UserNotificationServiceBean notifications() { return null; } + + @Override + public SystemConfig systemConfig() { + return null; + } + + @Override + public PrivateUrlServiceBean privateUrl() { + return null; + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java new file mode 100644 index 00000000000..a71ad732d8d --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java @@ -0,0 +1,162 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.engine.TestCommandContext; +import edu.harvard.iq.dataverse.engine.TestDataverseEngine; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class CreatePrivateUrlCommandTest { + + private TestDataverseEngine testEngine; + private Dataset dataset; + private final Long privateUrlAlreadyExists = 1l; + private final Long latestVersionIsNotDraft = 2l; + private final Long createDatasetLong = 3l; + + @Before + public void setUp() { + dataset = new Dataset(); + testEngine = new TestDataverseEngine(new TestCommandContext() { + @Override + public PrivateUrlServiceBean privateUrl() { + return new PrivateUrlServiceBean() { + + @Override + public PrivateUrl getPrivateUrlFromDatasetId(long datasetId) { + if (datasetId == privateUrlAlreadyExists) { + Dataset dataset = new Dataset(); + dataset.setId(privateUrlAlreadyExists); + String token = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(datasetId); + RoleAssignment roleAssignment = new RoleAssignment(null, privateUrlUser, dataset, token); + return new PrivateUrl(roleAssignment, dataset, "FIXME"); + } else if (datasetId == latestVersionIsNotDraft) { + return null; + } else { + return null; + } + } + + }; + } + + @Override + public DataverseRoleServiceBean roles() { + return new DataverseRoleServiceBean() { + + @Override + public DataverseRole findBuiltinRoleByAlias(String alias) { + return new DataverseRole(); + } + + @Override + public RoleAssignment save(RoleAssignment assignment) { + // no-op + return assignment; + } + + }; + } + + @Override + public SystemConfig systemConfig() { + return new SystemConfig() { + + @Override + public String getDataverseSiteUrl() { + return "https://dataverse.example.edu"; + } + + }; + + } + + } + ); + } + + @Test + public void testDatasetNull() { + dataset = null; + String expected = "Can't create Private URL. Dataset is null."; + String actual = null; + PrivateUrl privateUrl = null; + try { + privateUrl = testEngine.submit(new CreatePrivateUrlCommand(null, dataset)); + } catch (CommandException ex) { + actual = ex.getMessage(); + } + assertEquals(expected, actual); + assertNull(privateUrl); + } + + @Test + public void testAlreadyExists() { + dataset.setId(privateUrlAlreadyExists); + String expected = "Private URL already exists for dataset id " + privateUrlAlreadyExists + "."; + String actual = null; + PrivateUrl privateUrl = null; + try { + privateUrl = testEngine.submit(new CreatePrivateUrlCommand(null, dataset)); + } catch (CommandException ex) { + actual = ex.getMessage(); + } + assertEquals(expected, actual); + assertNull(privateUrl); + } + + @Test + public void testAttemptCreatePrivateUrlOnNonDraft() { + dataset = new Dataset(); + List versions = new ArrayList<>(); + DatasetVersion datasetVersion = new DatasetVersion(); + datasetVersion.setVersionState(DatasetVersion.VersionState.RELEASED); + versions.add(datasetVersion); + dataset.setVersions(versions); + dataset.setId(latestVersionIsNotDraft); + String expected = "Can't create Private URL because the latest version of dataset id " + latestVersionIsNotDraft + " is not a draft."; + String actual = null; + PrivateUrl privateUrl = null; + try { + privateUrl = testEngine.submit(new CreatePrivateUrlCommand(null, dataset)); + } catch (CommandException ex) { + actual = ex.getMessage(); + } + assertEquals(expected, actual); + assertNull(privateUrl); + } + + @Test + public void testCreatePrivateUrlSuccessfully() throws CommandException { + dataset = new Dataset(); + dataset.setId(createDatasetLong); + PrivateUrl privateUrl = testEngine.submit(new CreatePrivateUrlCommand(null, dataset)); + assertNotNull(privateUrl); + assertNotNull(privateUrl.getDataset()); + assertNotNull(privateUrl.getRoleAssignment()); + PrivateUrlUser expectedUser = new PrivateUrlUser(dataset.getId()); + assertEquals(expectedUser.getIdentifier(), privateUrl.getRoleAssignment().getAssigneeIdentifier()); + assertEquals(expectedUser.isSuperuser(), false); + assertEquals(expectedUser.isBuiltInUser(), false); + assertEquals(expectedUser.isAuthenticated(), false); + assertEquals(expectedUser.getDisplayInfo().getTitle(), "Private URL Enabled"); + assertNotNull(privateUrl.getToken()); + assertEquals("https://dataverse.example.edu/privateurl.xhtml?token=" + privateUrl.getToken(), privateUrl.getLink()); + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommandTest.java new file mode 100644 index 00000000000..74c8c269b4b --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePrivateUrlCommandTest.java @@ -0,0 +1,107 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.engine.TestCommandContext; +import edu.harvard.iq.dataverse.engine.TestDataverseEngine; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class DeletePrivateUrlCommandTest { + + private TestDataverseEngine testEngine; + Dataset dataset; + private final Long noPrivateUrlToDelete = 1l; + private final Long hasPrivateUrlToDelete = 2l; + + @Before + public void setUp() { + testEngine = new TestDataverseEngine(new TestCommandContext() { + @Override + public PrivateUrlServiceBean privateUrl() { + return new PrivateUrlServiceBean() { + + @Override + public PrivateUrl getPrivateUrlFromDatasetId(long datasetId) { + if (datasetId == noPrivateUrlToDelete) { + return null; + } else if (datasetId == hasPrivateUrlToDelete) { + Dataset dataset = new Dataset(); + dataset.setId(hasPrivateUrlToDelete); + String token = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(datasetId); + RoleAssignment roleAssignment = new RoleAssignment(null, privateUrlUser, dataset, token); + return new PrivateUrl(roleAssignment, dataset, "FIXME"); + } else { + return null; + } + } + + }; + } + + @Override + public DataverseRoleServiceBean roles() { + return new DataverseRoleServiceBean() { + @Override + public List directRoleAssignments(RoleAssignee roas, DvObject dvo) { + RoleAssignment roleAssignment = new RoleAssignment(); + List list = new ArrayList<>(); + list.add(roleAssignment); + return list; + } + + @Override + public void revoke(RoleAssignment ra) { + // no-op + } + + }; + } + + }); + } + + @Test + public void testDatasetNull() { + dataset = null; + String expected = "Can't delete Private URL. Dataset is null."; + String actual = null; + try { + testEngine.submit(new DeletePrivateUrlCommand(null, dataset)); + } catch (CommandException ex) { + actual = ex.getMessage(); + } + assertEquals(expected, actual); + } + + @Test + public void testSuccessfulDelete() { + dataset = new Dataset(); + dataset.setId(hasPrivateUrlToDelete); + String actual = null; + try { + testEngine.submit(new DeletePrivateUrlCommand(null, dataset)); + } catch (CommandException ex) { + actual = ex.getMessage(); + } + assertNull(actual); + /** + * @todo How would we confirm that the role assignement is actually + * gone? Really all we're testing above is that there was no + * IllegalCommandException from submitting the command. + */ + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommandTest.java new file mode 100644 index 00000000000..b5019807ac1 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetPrivateUrlCommandTest.java @@ -0,0 +1,70 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.engine.TestCommandContext; +import edu.harvard.iq.dataverse.engine.TestDataverseEngine; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import static org.junit.Assert.assertNull; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GetPrivateUrlCommandTest { + + private TestDataverseEngine testEngine; + Dataset dataset; + + public GetPrivateUrlCommandTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + testEngine = new TestDataverseEngine(new TestCommandContext() { + + @Override + public PrivateUrlServiceBean privateUrl() { + return new PrivateUrlServiceBean() { + + @Override + public PrivateUrl getPrivateUrlFromDatasetId(long datasetId) { + return null; + } + + }; + } + + }); + } + + @After + public void tearDown() { + } + + @Test + public void testDatasetWithoutAnId() throws Exception { + dataset = new Dataset(); + PrivateUrl privateUrl = testEngine.submit(new GetPrivateUrlCommand(null, dataset)); + assertNull(privateUrl); + } + + @Test + public void testDatasetWithAnId() throws Exception { + dataset = new Dataset(); + dataset.setId(42l); + PrivateUrl privateUrl = testEngine.submit(new GetPrivateUrlCommand(null, dataset)); + assertNull(privateUrl); + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtilTest.java new file mode 100644 index 00000000000..0544b66bb64 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlUtilTest.java @@ -0,0 +1,355 @@ +package edu.harvard.iq.dataverse.privateurl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; +import static org.junit.Assert.assertNull; +import org.junit.Before; + +public class PrivateUrlUtilTest { + + @Before + public void setUp() { + new PrivateUrlUtil(); + } + + @Test + public void testIdentifier2roleAssignee() { + RoleAssignee returnValueFromEmptyString = null; + try { + returnValueFromEmptyString = PrivateUrlUtil.identifier2roleAssignee(""); + } catch (Exception ex) { + assertEquals(ex.getClass(), IllegalArgumentException.class); + assertEquals(ex.getMessage(), "Could not find dataset id in ''"); + } + assertNull(returnValueFromEmptyString); + + RoleAssignee returnValueFromNonColon = null; + String peteIdentifier = "@pete"; + try { + returnValueFromNonColon = PrivateUrlUtil.identifier2roleAssignee(peteIdentifier); + } catch (Exception ex) { + assertEquals(ex.getClass(), IllegalArgumentException.class); + assertEquals(ex.getMessage(), "Could not find dataset id in '" + peteIdentifier + "'"); + } + assertNull(returnValueFromNonColon); + + RoleAssignee returnValueFromNonNumber = null; + String nonNumberIdentifier = PrivateUrlUser.PREFIX + "nonNumber"; + try { + returnValueFromNonNumber = PrivateUrlUtil.identifier2roleAssignee(nonNumberIdentifier); + } catch (Exception ex) { + assertEquals(ex.getClass(), IllegalArgumentException.class); + assertEquals(ex.getMessage(), "Could not find dataset id in '" + nonNumberIdentifier + "'"); + } + assertNull(returnValueFromNonNumber); + + RoleAssignee returnFromValidIdentifier = null; + String validIdentifier = PrivateUrlUser.PREFIX + 42; + returnFromValidIdentifier = PrivateUrlUtil.identifier2roleAssignee(validIdentifier); + assertNotNull(returnFromValidIdentifier); + assertEquals(":privateUrl42", returnFromValidIdentifier.getIdentifier()); + assertEquals("Private URL Enabled", returnFromValidIdentifier.getDisplayInfo().getTitle()); + Assert.assertTrue(returnFromValidIdentifier instanceof PrivateUrlUser); + PrivateUrlUser privateUrlUser42 = (PrivateUrlUser) returnFromValidIdentifier; + assertEquals(42, privateUrlUser42.getDatasetId()); + + } + + @Test + public void testGetDatasetFromRoleAssignmentNullRoleAssignment() { + assertNull(PrivateUrlUtil.getDatasetFromRoleAssignment(null)); + } + + @Test + public void testGetDatasetFromRoleAssignmentNullDefinitionPoint() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject nullDefinitionPoint = null; + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, nullDefinitionPoint, privateUrlToken); + assertNull(PrivateUrlUtil.getDatasetFromRoleAssignment(ra)); + } + + @Test + public void testGetDatasetFromRoleAssignmentNonDataset() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject nonDataset = new Dataverse(); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, nonDataset, privateUrlToken); + assertNull(PrivateUrlUtil.getDatasetFromRoleAssignment(ra)); + } + + @Test + public void testGetDatasetFromRoleAssignmentSuccess() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject dataset = new Dataset(); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + assertNotNull(PrivateUrlUtil.getDatasetFromRoleAssignment(ra)); + assertEquals(":privateUrl42", ra.getAssigneeIdentifier()); + } + + @Test + public void testGetDraftDatasetVersionFromRoleAssignmentNullRoleAssignement() { + assertNull(PrivateUrlUtil.getDraftDatasetVersionFromRoleAssignment(null)); + } + + @Test + public void testGetDraftDatasetVersionFromRoleAssignmentNullDataset() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject dataset = null; + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + DatasetVersion datasetVersion = PrivateUrlUtil.getDraftDatasetVersionFromRoleAssignment(ra); + assertNull(datasetVersion); + } + + @Test + public void testGetDraftDatasetVersionFromRoleAssignmentLastestIsNotDraft() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + Dataset dataset = new Dataset(); + List versions = new ArrayList<>(); + DatasetVersion datasetVersionIn = new DatasetVersion(); + datasetVersionIn.setVersionState(DatasetVersion.VersionState.RELEASED); + versions.add(datasetVersionIn); + dataset.setVersions(versions); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + DatasetVersion datasetVersionOut = PrivateUrlUtil.getDraftDatasetVersionFromRoleAssignment(ra); + assertNull(datasetVersionOut); + } + + @Test + public void testGetDraftDatasetVersionFromRoleAssignmentSuccess() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + Dataset dataset = new Dataset(); + List versions = new ArrayList<>(); + DatasetVersion datasetVersionIn = new DatasetVersion(); + datasetVersionIn.setVersionState(DatasetVersion.VersionState.DRAFT); + versions.add(datasetVersionIn); + dataset.setVersions(versions); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + DatasetVersion datasetVersionOut = PrivateUrlUtil.getDraftDatasetVersionFromRoleAssignment(ra); + assertNotNull(datasetVersionOut); + assertEquals(":privateUrl42", ra.getAssigneeIdentifier()); + } + + @Test + public void testGetUserFromRoleAssignmentNull() { + assertNull(PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(null)); + } + + @Test + public void testGetUserFromRoleAssignmentNonDataset() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUserIn = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUserIn; + DvObject nonDataset = new Dataverse(); + nonDataset.setId(123l); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, nonDataset, privateUrlToken); + PrivateUrlUser privateUrlUserOut = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(ra); + assertNull(privateUrlUserOut); + } + + @Test + public void testGetUserFromRoleAssignmentSucess() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUserIn = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUserIn; + DvObject dataset = new Dataset(); + dataset.setId(123l); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + PrivateUrlUser privateUrlUserOut = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(ra); + assertNotNull(privateUrlUserOut); + } + + @Test + public void testGetPrivateUrlRedirectDataFail() { + DataverseRole aRole = null; + long datasetId = 42; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(datasetId); + RoleAssignee anAssignee = privateUrlUser; + Dataset dataset = new Dataset(); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + ra.setDefinitionPoint(null); + PrivateUrlRedirectData privateUrlRedirectData = null; + privateUrlRedirectData = PrivateUrlUtil.getPrivateUrlRedirectData(ra); + assertNull(privateUrlRedirectData); + } + + @Test + public void testGetPrivateUrlRedirectDataSuccess() { + DataverseRole aRole = null; + long datasetId = 42; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(datasetId); + RoleAssignee anAssignee = privateUrlUser; + Dataset dataset = new Dataset(); + dataset.setProtocol("doi"); + dataset.setAuthority("10.5072/FK2"); + dataset.setIdentifier("3L33T"); + dataset.setId(datasetId); + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + PrivateUrlRedirectData privateUrlRedirectData = PrivateUrlUtil.getPrivateUrlRedirectData(ra); + assertNotNull(privateUrlRedirectData); + assertEquals("/dataset.xhtml?persistentId=doi:10.5072/FK2/3L33T&version=DRAFT", privateUrlRedirectData.getDraftDatasetPageToBeRedirectedTo()); + assertEquals(privateUrlUser.getIdentifier(), privateUrlRedirectData.getPrivateUrlUser().getIdentifier()); + } + + @Test + public void testGetDraftUrlDraftNull() { + assertEquals("UNKNOWN", PrivateUrlUtil.getDraftUrl(null)); + } + + @Test + public void testGetDraftUrlDatasetNull() { + DatasetVersion draft = new DatasetVersion(); + draft.setDataset(null); + assertEquals("UNKNOWN", PrivateUrlUtil.getDraftUrl(draft)); + } + + @Test + public void testGetDraftUrlNoGlobalId() throws Exception { + DatasetVersion draft = new DatasetVersion(); + Dataset dataset = new Dataset(); + draft.setDataset(dataset); + assertEquals("UNKNOWN", PrivateUrlUtil.getDraftUrl(draft)); + } + + @Test + public void testGetDraftUrlSuccess() throws Exception { + DatasetVersion draft = new DatasetVersion(); + Dataset dataset = new Dataset(); + dataset.setProtocol("doi"); + dataset.setAuthority("10.5072/FK2"); + dataset.setIdentifier("3L33T"); + draft.setDataset(dataset); + assertEquals("/dataset.xhtml?persistentId=doi:10.5072/FK2/3L33T&version=DRAFT", PrivateUrlUtil.getDraftUrl(draft)); + } + + @Test + public void testGetPrivateUrlRedirectDataConstructor() throws Exception { + Exception exception1 = null; + try { + PrivateUrlRedirectData privateUrlRedirectData = new PrivateUrlRedirectData(null, null); + } catch (Exception ex) { + exception1 = ex; + } + assertNotNull(exception1); + Exception exception2 = null; + try { + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + PrivateUrlRedirectData privateUrlRedirectData = new PrivateUrlRedirectData(privateUrlUser, null); + } catch (Exception ex) { + exception2 = ex; + } + assertNotNull(exception2); + } + + @Test + public void testGetPrivateUrlFromRoleAssignmentNoSiteUrl() { + String dataverseSiteUrl = null; + RoleAssignment ra = null; + PrivateUrl privateUrl = PrivateUrlUtil.getPrivateUrlFromRoleAssignment(ra, dataverseSiteUrl); + assertNull(privateUrl); + } + + @Test + public void testGetPrivateUrlFromRoleAssignmentDatasetNull() { + String dataverseSiteUrl = "https://dataverse.example.edu"; + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject dataset = null; + String privateUrlToken = null; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + PrivateUrl privateUrl = PrivateUrlUtil.getPrivateUrlFromRoleAssignment(ra, dataverseSiteUrl); + assertNull(privateUrl); + } + + @Test + public void testGetPrivateUrlFromRoleAssignmentSuccess() { + String dataverseSiteUrl = "https://dataverse.example.edu"; + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUser; + DvObject dataset = new Dataset(); + dataset.setId(42l); + String privateUrlToken = "cd71e9d7-73a7-4ec8-b890-3d00499e8693"; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + PrivateUrl privateUrl = PrivateUrlUtil.getPrivateUrlFromRoleAssignment(ra, dataverseSiteUrl); + assertNotNull(privateUrl); + assertEquals(new Long(42), privateUrl.getDataset().getId()); + assertEquals("https://dataverse.example.edu/privateurl.xhtml?token=cd71e9d7-73a7-4ec8-b890-3d00499e8693", privateUrl.getLink()); + } + + @Test + public void testGetPrivateUrlUserFromRoleAssignmentAndAssigneeNull() { + PrivateUrlUser privateUrl = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(null, null); + assertNull(privateUrl); + } + + @Test + public void testGetPrivateUrlUserFromRoleAssignmentAndAssigneeNonPrivateUrlUser() { + DataverseRole aRole = null; + RoleAssignee assignee = GuestUser.get(); + DvObject dataset = new Dataset(); + String privateUrlToken = "cd71e9d7-73a7-4ec8-b890-3d00499e8693"; + RoleAssignment assignment = new RoleAssignment(aRole, assignee, dataset, privateUrlToken); + PrivateUrlUser privateUrl = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(assignment, assignee); + assertNull(privateUrl); + } + + @Test + public void testGetPrivateUrlUserFromRoleAssignmentAndAssigneeSuccess() { + DataverseRole aRole = null; + PrivateUrlUser privateUrlUser = new PrivateUrlUser(42); + RoleAssignee assignee = privateUrlUser; + DvObject dataset = new Dataset(); + dataset.setId(42l); + String privateUrlToken = "cd71e9d7-73a7-4ec8-b890-3d00499e8693"; + RoleAssignment assignment = new RoleAssignment(aRole, assignee, dataset, privateUrlToken); + PrivateUrlUser privateUrl = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(assignment, assignee); + assertNotNull(privateUrl); + } + + @Test + public void testGetRequiredPermissions() { + CreatePrivateUrlCommand createPrivateUrlCommand = new CreatePrivateUrlCommand(null, null); + CommandException ex = new CommandException(null, createPrivateUrlCommand); + List strings = PrivateUrlUtil.getRequiredPermissions(ex); + assertEquals(Arrays.asList("ManageDatasetPermissions"), strings); + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java new file mode 100644 index 00000000000..148ac95b4dd --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java @@ -0,0 +1,53 @@ +package edu.harvard.iq.dataverse.util.json; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; + +public class JsonPrinterTest { + + @Test + public void testJson_RoleAssignment() { + DataverseRole aRole = new DataverseRole(); + PrivateUrlUser privateUrlUserIn = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUserIn; + Dataset dataset = new Dataset(); + dataset.setId(123l); + String privateUrlToken = "e1d53cf6-794a-457a-9709-7c07629a8267"; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + JsonObjectBuilder job = JsonPrinter.json(ra); + assertNotNull(job); + JsonObject jsonObject = job.build(); + assertEquals(":privateUrl42", jsonObject.getString("assignee")); + assertEquals(123, jsonObject.getInt("definitionPointId")); + assertEquals("e1d53cf6-794a-457a-9709-7c07629a8267", jsonObject.getString("privateUrlToken")); + } + + @Test + public void testJson_PrivateUrl() { + DataverseRole aRole = new DataverseRole(); + PrivateUrlUser privateUrlUserIn = new PrivateUrlUser(42); + RoleAssignee anAssignee = privateUrlUserIn; + Dataset dataset = new Dataset(); + String privateUrlToken = "e1d53cf6-794a-457a-9709-7c07629a8267"; + RoleAssignment ra = new RoleAssignment(aRole, anAssignee, dataset, privateUrlToken); + String dataverseSiteUrl = "https://dataverse.example.edu"; + PrivateUrl privateUrl = new PrivateUrl(ra, dataset, dataverseSiteUrl); + JsonObjectBuilder job = JsonPrinter.json(privateUrl); + assertNotNull(job); + JsonObject jsonObject = job.build(); + assertEquals("e1d53cf6-794a-457a-9709-7c07629a8267", jsonObject.getString("token")); + assertEquals("https://dataverse.example.edu/privateurl.xhtml?token=e1d53cf6-794a-457a-9709-7c07629a8267", jsonObject.getString("link")); + assertEquals("e1d53cf6-794a-457a-9709-7c07629a8267", jsonObject.getJsonObject("roleAssignment").getString("privateUrlToken")); + assertEquals(":privateUrl42", jsonObject.getJsonObject("roleAssignment").getString("assignee")); + } + +}