Skip to content

Commit

Permalink
Closes #2540 Create double-blind-review-ready Anonymized Private URL
Browse files Browse the repository at this point in the history
  • Loading branch information
lbownik authored and lbownik committed Dec 30, 2024
1 parent ea5306a commit 0fc1ef1
Show file tree
Hide file tree
Showing 31 changed files with 738 additions and 376 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.harvard.iq.dataverse.persistence.user;

import edu.harvard.iq.dataverse.persistence.DvObject;
import edu.harvard.iq.dataverse.persistence.JpaEntity;
import edu.harvard.iq.dataverse.persistence.config.LocaleConverter;
import edu.harvard.iq.dataverse.persistence.config.ValidateEmail;
Expand Down Expand Up @@ -194,6 +195,16 @@ public List<AcceptedConsent> getAcceptedConsents() {
public boolean isSuperuser() {
return superuser;
}

@Override
public boolean isAnonymized() {
return false;
}

@Override
public boolean isAffiliatedWith(final DvObject object) {
return object.getReleaseUser().equals(this);
}

public AuthenticatedUserLookup getAuthenticatedUserLookup() {
return authenticatedUserLookup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,4 @@ public boolean doesDvObjectClassHavePermissionForObject(Class<? extends DvObject
return false;

} // doesDvObjectClassHavePermissionForObject


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package edu.harvard.iq.dataverse.persistence.user;

import edu.harvard.iq.dataverse.persistence.DvObject;

/**
* Guest user in the system. There's only one, so you get it with the static getter {@link #get()} (singleton pattern).
*
Expand Down Expand Up @@ -35,6 +37,16 @@ public boolean isAuthenticated() {
public boolean isSuperuser() {
return false;
}

@Override
public boolean isAnonymized() {
return false;
}

@Override
public boolean isAffiliatedWith(final DvObject object) {
return false;
}

@Override
public boolean equals(Object o) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
package edu.harvard.iq.dataverse.persistence.user;

import edu.harvard.iq.dataverse.common.BundleUtil;
import static edu.harvard.iq.dataverse.common.BundleUtil.getStringFromBundle;

import edu.harvard.iq.dataverse.persistence.DvObject;

/**
* 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.)
* which is stored in the roleassignment table.)
*/
@SuppressWarnings("serial")
public class PrivateUrlUser implements User {

public static final String PREFIX = "#";

/**
* In the future, this could probably be dvObjectId rather than datasetId,
* if necessary. It's really just roleAssignment.getDefinitionPoint(), which
* is a DvObject.
* 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;
private final boolean anonymized;

public PrivateUrlUser(long datasetId) {
public PrivateUrlUser(final long datasetId) {
this.datasetId = datasetId;
this.anonymized = false;
}

public long getDatasetId() {
return datasetId;
public PrivateUrlUser(final long datasetId, final boolean anonymized) {
this.datasetId = datasetId;
this.anonymized = anonymized;
}

public long getDatasetId() {
return this.datasetId;
}

/**
* By always returning false for isAuthenticated(), we prevent a
* name from appearing in the corner as well as preventing an account page
Expand All @@ -48,14 +57,22 @@ public boolean isSuperuser() {

@Override
public String getIdentifier() {
return PREFIX + datasetId;
return PREFIX + this.datasetId;
}

@Override
public RoleAssigneeDisplayInfo getDisplayInfo() {
String title = BundleUtil.getStringFromBundle("dataset.privateurl.roleassigeeTitle");
return new RoleAssigneeDisplayInfo(title, null);
public boolean isAnonymized() {
return this.anonymized;
}

@Override
public boolean isAffiliatedWith(final DvObject object) {
return this.datasetId == object.getId();
}


@Override
public RoleAssigneeDisplayInfo getDisplayInfo() {
return new RoleAssigneeDisplayInfo(
getStringFromBundle("dataset.privateurl.roleassigeeTitle"), null);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package edu.harvard.iq.dataverse.persistence.user;

import edu.harvard.iq.dataverse.persistence.DvObject;
import edu.harvard.iq.dataverse.persistence.JpaEntity;
import static javax.persistence.CascadeType.MERGE;
import static javax.persistence.GenerationType.IDENTITY;

import java.util.Objects;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
Expand All @@ -16,7 +16,9 @@
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import java.util.Objects;

import edu.harvard.iq.dataverse.persistence.DvObject;
import edu.harvard.iq.dataverse.persistence.JpaEntity;

/**
* A role of a user in a Dataverse. A User may have many roles in a given Dataverse.
Expand All @@ -25,6 +27,7 @@
*
* @author michael
*/
@SuppressWarnings("serial")
@Entity
@Table(
uniqueConstraints = @UniqueConstraint(columnNames = {"assigneeIdentifier", "role_id", "definitionPoint_id"})
Expand All @@ -34,7 +37,7 @@
)
@NamedQueries({
@NamedQuery(name = "RoleAssignment.listByAssigneeIdentifier_DefinitionPointId",
query = "SELECT r FROM RoleAssignment r WHERE r.assigneeIdentifier=:assigneeIdentifier AND r.definitionPoint.id=:definitionPointId"),
query = "SELECT r FROM RoleAssignment r WHERE r.assigneeIdentifier=:assigneeIdentifier AND r.definitionPoint.id=:definitionPointId AND r.anonymized = :anonymized"),
@NamedQuery(name = "RoleAssignment.listByAssigneeIdentifier_DefinitionPointId_RoleId",
query = "SELECT r FROM RoleAssignment r WHERE r.assigneeIdentifier=:assigneeIdentifier AND r.definitionPoint.id=:definitionPointId and r.role.id=:roleId"),
@NamedQuery(name = "RoleAssignment.listByAssigneeIdentifier",
Expand All @@ -50,31 +53,40 @@
})
public class RoleAssignment implements java.io.Serializable, JpaEntity<Long> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GeneratedValue(strategy = IDENTITY)
private Long id;

@Column(nullable = false)
private String assigneeIdentifier;

@ManyToOne(cascade = {CascadeType.MERGE})
@ManyToOne(cascade = {MERGE})
@JoinColumn(nullable = false)
private DataverseRole role;

@ManyToOne(cascade = {CascadeType.MERGE})
@ManyToOne(cascade = {MERGE})
@JoinColumn(nullable = false)
private DvObject definitionPoint;

@Column(nullable = true)
private String privateUrlToken;

private boolean anonymized;

public RoleAssignment() {
}

public RoleAssignment(DataverseRole aRole, RoleAssignee anAssignee, DvObject aDefinitionPoint, String privateUrlToken) {
public RoleAssignment(DataverseRole aRole, RoleAssignee anAssignee,
DvObject aDefinitionPoint, String privateUrlToken) {
this(aRole, anAssignee, aDefinitionPoint, privateUrlToken, false);
}

public RoleAssignment(DataverseRole aRole, RoleAssignee anAssignee,
DvObject aDefinitionPoint, String privateUrlToken, boolean anonymized) {
role = aRole;
assigneeIdentifier = anAssignee.getIdentifier();
definitionPoint = aDefinitionPoint;
this.privateUrlToken = privateUrlToken;
this.anonymized = anonymized;
}

public Long getId() {
Expand Down Expand Up @@ -113,6 +125,14 @@ public String getPrivateUrlToken() {
return privateUrlToken;
}

public boolean isAnonymized() {
return this.anonymized;
}

public void setAnonymized(final boolean anonymized) {
this.anonymized = anonymized;
}

@Override
public int hashCode() {
int hash = 7;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package edu.harvard.iq.dataverse.persistence.user;

import edu.harvard.iq.dataverse.persistence.JpaRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.harvard.iq.dataverse.persistence.dataset.Dataset;

import javax.ejb.Singleton;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import static java.util.stream.Collectors.joining;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Singleton
public class RoleAssignmentRepository extends JpaRepository<Long, RoleAssignment> {
private static final Logger logger = LoggerFactory.getLogger(RoleAssignmentRepository.class);

// -------------------- CONSTRUCTORS --------------------

Expand Down Expand Up @@ -69,9 +71,54 @@ public List<Integer> findDataversesWithUserPermitted(List<String> identifiers) {
String query = "SELECT id FROM dvobject WHERE dtype = 'Dataverse' " +
"and id in (select definitionpoint_id from roleassignment " +
"where assigneeidentifier in ("
+ identifiers.stream().map(i -> "'" + i + "'").collect(Collectors.joining(",")) + "));";
logger.info("query: {}", query);
+ identifiers.stream().map(i -> "'" + i + "'").collect(joining(",")) + "));";
Query nativeQuery = em.createNativeQuery(query);
return (List<Integer>) nativeQuery.getResultList();
}

/**
* @return A RoleAssignment or null.
* @todo This might be a good place for Optional.
*/
public RoleAssignment getRoleAssignmentFromPrivateUrlToken(
final String privateUrlToken) {
if (privateUrlToken == null) {
return null;
} else {
try {
return this.em
.createNamedQuery("RoleAssignment.listByPrivateUrlToken",
RoleAssignment.class)
.setParameter("privateUrlToken", privateUrlToken)
.getSingleResult();
} catch (final 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.
*/
public RoleAssignment getPrivateUrlRoleAssignmentFromDataset(
final Dataset dataset, final boolean anonymized) {
if (dataset == null) {
return null;
} else {
try {
return this.em.createNamedQuery(
"RoleAssignment.listByAssigneeIdentifier_DefinitionPointId",
RoleAssignment.class)
.setParameter("assigneeIdentifier",
new PrivateUrlUser(dataset.getId()).getIdentifier())
.setParameter("definitionPointId", dataset.getId())
.setParameter("anonymized", anonymized)
.getSingleResult();
} catch (final NoResultException | NonUniqueResultException ex) {
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.io.Serializable;

import edu.harvard.iq.dataverse.persistence.DvObject;

/**
* A user of the dataverse system. Intuitively a single real person in real
* life, but some corner cases exist (e.g. {@link GuestUser}, who stands for
Expand All @@ -12,5 +14,8 @@ public interface User extends RoleAssignee, Serializable {
boolean isAuthenticated();

boolean isSuperuser();


boolean isAnonymized();

boolean isAffiliatedWith(DvObject object);
}
5 changes: 5 additions & 0 deletions dataverse-persistence/src/main/resources/Bundle_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,7 @@ dataset.editBtn.itemLabel.selectGuestbook=Select Guestbook
dataset.editBtn.itemLabel.permissions=Permissions
dataset.editBtn.itemLabel.thumbnailsAndWidgets=Thumbnails + Widgets
dataset.editBtn.itemLabel.privateUrl=Private URL
dataset.editBtn.itemLabel.privateAnonymizedUrl=Private anonymized URL
dataset.editBtn.itemLabel.embargo=Embargo
dataset.editBtn.itemLabel.permissionsDataset=Dataset
dataset.editBtn.itemLabel.permissionsFile=Restricted Files
Expand Down Expand Up @@ -1746,6 +1747,7 @@ 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.title=Dataset Private URL
dataset.privateurl.anonymized.infoMessageAuthor.title=Dataset Anonymized Private URL
dataset.privateurl.infoMessageAuthor.details=Privately share this dataset before it is publicly available: {0}
dataset.privateurl.infoMessageReviewer=Dataset Private URL - This dataset is being privately shared. You will not be able to access it when logged into your Dataverse account.
dataset.privateurl.infoMessageReviewer.title=Dataset Private URL
Expand All @@ -1761,6 +1763,9 @@ dataset.privateurl.roleassigeeTitle=Private URL Enabled
dataset.privateurl.createdSuccess=Success!
dataset.privateurl.disabledSuccess=You have successfully disabled the Private URL for this dataset.
dataset.privateurl.noPermToCreate=To create a Private URL you must have the following permissions: {0}.
dataset.anonymized.privateurl.header=Dataset Anonymized Private URL
dataset.anonymized.privateurl.tip=Use a Anonymized Private URL to allow those without Dataverse accounts to access your dataset. For more information about the Private URL feature, please refer to the <a href="{0}" title="Private URL for Reviewing Dataset - Dataverse User Guide" target="_blank">User Guide</a>.
dataset.anonymized.privateurl.warning="WARNING. This dataset has at least one published version. Those who have access to the Anonymized Private URL for this dataset may be able to use its accessible metadata to look up the full, not anonymized version of this dataset.
dataset.embargo.datasetSummary.title=Embargo
dataset.embargo.datasetSummary.tip=In an embargoed dataset files remain temporarily unavailable.
dataset.embargo.datasetSummary.message=Files in this dataset will be available from {0}.
Expand Down
5 changes: 5 additions & 0 deletions dataverse-persistence/src/main/resources/Bundle_pl.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,7 @@ dataset.editBtn.itemLabel.selectGuestbook=Przypisz Ksi\u0119g\u0119 Go\u015Bci
dataset.editBtn.itemLabel.permissions=Uprawnienia
dataset.editBtn.itemLabel.thumbnailsAndWidgets=Miniaturka + Wid\u017Cety
dataset.editBtn.itemLabel.privateUrl=Prywatny adres URL
dataset.editBtn.itemLabel.privateAnonymizedUrl=Prywatny anonimowy adres URL
dataset.editBtn.itemLabel.embargo=Embargo
dataset.editBtn.itemLabel.permissionsDataset=Zbi\u00F3r danych
dataset.editBtn.itemLabel.permissionsFile=Zastrze\u017Cone pliki
Expand Down Expand Up @@ -1726,6 +1727,7 @@ dataset.noValidSelectedFilesForDownload=Nie mo\u017Cna pobra\u0107 wybranych zas
dataset.mixedSelectedFilesForDownload=Nie mo\u017Cna pobra\u0107 wybranych zastrze\u017Conych plik\u00F3w, poniewa\u017C nie przyznano Ci do nich dost\u0119pu.
dataset.downloadUnrestricted=Naci\u015Bnij "Dalej" aby pobra\u0107 pliki, do pobrania kt\u00F3rych posiadasz odpowiednie uprawnienia.
dataset.privateurl.infoMessageAuthor.title=Prywatny URL zbioru danych
dataset.privateurl.anonymized.infoMessageAuthor.title=Prywatny anonimizowany URL zbioru danych
dataset.privateurl.infoMessageAuthor.details=Udost\u0119pnij prywatnie ten zbi\u00F3r danych jeszcze zanim stanie si\u0119 publicznie dost\u0119pny: {0}
dataset.privateurl.infoMessageReviewer.title=Prywatny URL zbioru danych
dataset.privateurl.infoMessageReviewer.details=Ten zbi\u00F3r danych jest udost\u0119pniany prywatnie. Nie b\u0119dziesz mie\u0107 do niego dost\u0119pu po zalogowaniu na konto w repozytorium.
Expand All @@ -1740,6 +1742,9 @@ dataset.privateurl.roleassigeeTitle=Prywatny URL
dataset.privateurl.createdSuccess=Operacja zako\u0144czona powodzeniem.
dataset.privateurl.disabledSuccess=Uda\u0142o Ci si\u0119 dezaktywowa\u0107 prywatny URL tego zbioru danych.
dataset.privateurl.noPermToCreate=Aby stworzy\u0107 prywatny URL, musisz mie\u0107 nast\u0119puj\u0105ce uprawnienia: {0}.
dataset.anonymized.privateurl.header=Prywatny Anonimowy URL zbioru danych
dataset.anonymized.privateurl.tip=Skorzystaj z prywatnego URL by osobom nieposiadaj\u0105cym konta w repozytorium umo\u017Cliwi\u0107 dost\u0119p do Twojego zbioru danych. Wi\u0119cej informacji na temat prywatnego URL mo\u017Cna znale\u017A\u0107 w <a href="{0}" title="Private URL for Reviewing Dataset - Dataverse User Guide" target="_blank">"Poradniku u\u017Cytkownika"</a>.
dataset.anonymized.privateurl.warning="UWAGA. Tena zbiór danych posiada opublikowan\u0105 wersj\u0119. Osoby u\u017Cywaj\u0105ce Zanonymizowanego Prywatnego adresu URL mog\u0105 by\u0107 w stanie uzyska\u0107 pe\u0142ny, niezanonimizowany dost\u0119p do tego zbioru.
dataset.embargo.datasetSummary.title=Embargo
dataset.embargo.datasetSummary.tip=W zbiorze obj\u0119tym embargiem pliki pozostaj\u0105 czasowo niedost\u0119pne.
dataset.embargo.datasetSummary.message=Pliki w tym zbiorze danych b\u0119d\u0105 dost\u0119pne od {0}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
alter table roleassignment add column if not exists anonymized boolean not null default false;
ALTER TABLE roleassignment DROP CONSTRAINT unq_roleassignment_0;
alter table roleassignment add CONSTRAINT unq_roleassignment_0 UNIQUE (assigneeidentifier, role_id, definitionpoint_id, anonymized);
Loading

0 comments on commit 0fc1ef1

Please sign in to comment.