Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## Bug ##
Preview URL users could not download files from the dataset being previewed if a guestbook was assigned to that dataset. This is now fixed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.dataaccess.DataAccess;
import edu.harvard.iq.dataverse.dataaccess.StorageIO;
Expand All @@ -14,16 +15,19 @@
import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
import edu.harvard.iq.dataverse.settings.JvmSettings;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.*;
import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.primefaces.PrimeFaces;

Expand Down Expand Up @@ -314,6 +318,14 @@ private void redirectToDownloadAPI(String downloadType, Long fileId, boolean gue
} else {
logger.fine("Redirecting to file download url: " + fileDownloadUrl);
try {
// Sign URL for preview url user

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you set the token in the PrivateUrlUser, is there any reason you can't generate the required signed URL in the same method, e.g.

private Response returnSignedUrl(ContainerRequestContext crc, UriInfo uriInfo, User user, String id, String gbrids) {
// Create the signed URL
String userIdentifier = null;
String key = null;
if (user != null && user instanceof AuthenticatedUser) {
AuthenticatedUser requestor = (AuthenticatedUser) user;
userIdentifier = requestor.getUserIdentifier();
// Find the latest token: Use for signing
// Could be null if no token was generated: Generate one to be used for signing (expire in 1 minute to match timeout in signedUrl)
// Could be expired: The user was already authenticated (possible by bearer token). Only used for signing so we don't care
ApiToken apiToken = authSvc.findApiTokenByUser(requestor);
if (apiToken == null) {
logger.fine("Generating temporary API token for user " + userIdentifier);
apiToken = authSvc.generateApiTokenForUser(requestor, AuthenticationServiceBean.INTERVAL.MINUTES, GUESTBOOK_RESPONSE_SIGNEDURL_TIMEOUT_MINUTES);
}
if (apiToken != null) {
key = apiToken.getTokenString();
}
} else {
// Guest
userIdentifier = "guest";
// Note: In order for the key to match we need to replace ":persistentId" with the actual file id since that is what will be sent in via the signed url.
key = URLDecoder.decode(uriInfo.getAbsolutePath().toASCIIString())
.replace(":persistentId", id); //TODO find a better one for here and in SignedUrlAuthMechanism.java
}
? (If not the URL munging here has to change as localhost:8080 won't work.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fully understanding your comment. The signing happens in 2 places.

  1. in DatasetPage (FileDownloadServiceBean) when the preview user tries to download a file. The redirect is simply "/api/access/..." (no scheme, service, port)
  2. In access when the api call comes in. the url is "http://localhost:8080/api/v1/access/..."
    When the access call compares the token hash of the signed url it doesn't match if the signed token was generated using "/api/access/..."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I'm misunderstanding. My assumption is that this case is similar to the case of a dataset creator downloading from the draft - just a different type of user and different key. So I expected to see some code right where signing for an authenticated user happened to also sign for a privateUulUser - same url, same signing code, just a difference in getting the signing key for this case - versus a new method to generate and sign a URL for this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dataset creator downloading from draft includes the auth key in the header. The preview user has a token in the query params. in order to propagate the fact that it's a preview user and not a guest the url needs to be signed. This allows the SignedUrlAuthMechanism to authenticate a PrivateUrlUser based on the user=!{datasetId} and the token from the datasetId

User user = session.getUser();
if (user != null && (user instanceof PrivateUrlUser)) {
PrivateUrlUser privateUrlUser = (PrivateUrlUser) user;
String key = JvmSettings.API_SIGNING_SECRET.lookupOptional().orElse("") + privateUrlUser.getToken();
// Signing requires the full url path
fileDownloadUrl = UrlSignerUtil.signUrl(getFullUrlForSigning(fileDownloadUrl), 1, privateUrlUser.getIdentifier(), "GET", key);
}
FacesContext.getCurrentInstance().getExternalContext().redirect(fileDownloadUrl);
} catch (IOException ex) {
logger.info("Failed to issue a redirect to file download url (" + fileDownloadUrl + "): " + ex);
Expand All @@ -337,7 +349,23 @@ public void redirectToAuxFileDownloadAPI(Long fileId, String formatTag, String f
logger.info("Failed to issue a redirect to aux file download url (" + fileDownloadUrl + "): " + ex);
}
}


private String getFullUrlForSigning(String path) {
ExternalContext extContext = FacesContext.getCurrentInstance().getExternalContext();

// Get the base server and port details
HttpServletRequest request = (HttpServletRequest) extContext.getRequest();
String scheme = request.getScheme();
String serverName = request.getServerName();
int port = request.getServerPort();

// Build the full base URL + path and an API version (default is v1)
StringBuilder fullUrl = new StringBuilder();
fullUrl.append(scheme).append("://").append(serverName).append(":").append(port)
.append(path.replace("/api/access/", "/api/v1/access/"));

return fullUrl.toString();
}
/**
* Launch an "explore" tool which is a type of ExternalTool such as
* Data Explorer. This method may be invoked directly from the
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/edu/harvard/iq/dataverse/api/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.GuestUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.authorization.users.*;
import edu.harvard.iq.dataverse.dataaccess.*;
import edu.harvard.iq.dataverse.datavariable.DataVariable;
import edu.harvard.iq.dataverse.datavariable.VariableServiceBean;
Expand Down Expand Up @@ -2237,7 +2234,12 @@ public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc,
private boolean checkGuestbookRequiredResponse(User user, UriInfo uriInfo, DataFile df, String gbrids) throws WebApplicationException {
// Check if guestbook response is required
Dataset d = df.getOwner();
boolean required = df.getOwner().hasEnabledGuestbook() && !d.getEffectiveGuestbookEntryAtRequest();
boolean exempt = false;
// PrivateUrlUser access to draft files is exempt from guestbook responses in JSF https://github.com/IQSS/dataverse/issues/12535
if (user instanceof PrivateUrlUser) {
exempt = (df.getOwner().getId() == ((PrivateUrlUser) user).getDatasetId());
}
boolean required = !exempt && df.getOwner().hasEnabledGuestbook() && !d.getEffectiveGuestbookEntryAtRequest();
boolean wasWrittenInPost = false;
if (required) {
User requestor = getRequestor(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class PrivateUrlUser implements User {
* is a DvObject.
*/
private final long datasetId;
private final boolean anonymizedAccess;
private final boolean anonymizedAccess;
private String token = null;

public PrivateUrlUser(long datasetId) {
this(datasetId, false);
Expand All @@ -38,7 +39,14 @@ public long getDatasetId() {
public boolean hasAnonymizedAccess() {
return anonymizedAccess;
}


public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}

/**
* By always returning false for isAuthenticated(), we prevent a
* name from appearing in the corner as well as preventing an account page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,23 @@ public PrivateUrl getPrivateUrlFromDatasetId(long datasetId) {
* @return A PrivateUrlUser if one can be found using the token or null.
*/
public PrivateUrlUser getPrivateUrlUserFromToken(String token) {
return PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(getRoleAssignmentFromPrivateUrlToken(token));
PrivateUrlUser user = PrivateUrlUtil.getPrivateUrlUserFromRoleAssignment(getRoleAssignmentFromPrivateUrlToken(token));
if (user != null) {
user.setToken(token);
}
return user;
}

/**
* @return PrivateUrlRedirectData if it can be found using the token or
* null.
*/
public PrivateUrlRedirectData getPrivateUrlRedirectDataFromToken(String token) {
return PrivateUrlUtil.getPrivateUrlRedirectData(getRoleAssignmentFromPrivateUrlToken(token));
PrivateUrlRedirectData privateUrlRedirectData = PrivateUrlUtil.getPrivateUrlRedirectData(getRoleAssignmentFromPrivateUrlToken(token));
if (privateUrlRedirectData != null && privateUrlRedirectData.getPrivateUrlUser() != null) {
privateUrlRedirectData.getPrivateUrlUser().setToken(token);
}
return privateUrlRedirectData;
}

/**
Expand Down
Loading