From c38c37e56a70e6ff629c9bf3fcf1216c477adf2b Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:24:41 -0400 Subject: [PATCH 1/8] Allow Preview User to download files without requiring a Guestbook Response --- ...ut-guestbook-response-for-preview-user2.md | 2 ++ .../edu/harvard/iq/dataverse/api/Access.java | 33 ++++++------------- .../api/auth/SessionCookieAuthMechanism.java | 14 +++++++- 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md diff --git a/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md b/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md new file mode 100644 index 00000000000..5c2eaa05a9b --- /dev/null +++ b/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md @@ -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. 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 7d12e5e8b3f..d29bdba9d60 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -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; @@ -197,7 +194,7 @@ public BundleDownloadInstance datafileBundle(@Context ContainerRequestContext cr if (gbrecs != true && df.isReleased()) { // Write Guestbook record if not done previously and file is released - GuestbookResponse gbr = guestbookResponseService.initAPIGuestbookResponse(df.getOwner(), df, session, getRequestor(req.getUser())); + GuestbookResponse gbr = guestbookResponseService.initAPIGuestbookResponse(df.getOwner(), df, session, req.getUser()); guestbookResponseService.save(gbr); MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, df); mdcLogService.logEntry(entry); @@ -327,7 +324,7 @@ public Response datafile(@Context ContainerRequestContext crc, if (gbrecs != true && df.isReleased()){ // Write Guestbook record if not done previously and file is released - gbr = guestbookResponseService.initAPIGuestbookResponse(df.getOwner(), df, session, getRequestor(req.getUser())); + gbr = guestbookResponseService.initAPIGuestbookResponse(df.getOwner(), df, session, req.getUser()); } DownloadInfo dInfo = new DownloadInfo(df); @@ -1234,7 +1231,7 @@ private Response downloadDatafiles(ContainerRequestContext crc, String body, boo String customZipServiceUrl = settingsService.getValueForKey(SettingsServiceBean.Key.CustomZipDownloadServiceUrl); boolean useCustomZipService = customZipServiceUrl != null; - User user = getRequestor(getRequestUser(crc)); + User user = getRequestUser(crc); DataverseRequest req = createDataverseRequest(user); Boolean getOrig = false; @@ -2236,12 +2233,11 @@ 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(); + Dataset ds = df.getOwner(); + boolean required = ds.hasEnabledGuestbook() && !ds.getEffectiveGuestbookEntryAtRequest() && !(user instanceof PrivateUrlUser); boolean wasWrittenInPost = false; if (required) { - User requestor = getRequestor(user); - if (requestor instanceof AuthenticatedUser && permissionService.userOn(requestor, df.getOwner()).has(Permission.EditDataset)) { + if (user instanceof AuthenticatedUser && permissionService.userOn(user, ds).has(Permission.EditDataset)) { required = false; } // Check if we are downloading a thumbnail image which doesn't require a guestbook response @@ -2258,7 +2254,7 @@ private boolean checkGuestbookRequiredResponse(User user, UriInfo uriInfo, DataF throw new NotFoundException("GuestbookResponse Not Found for id:" + gbrids); } Long delta = Instant.now().toEpochMilli() - gbr.getResponseTime().getTime(); - wasWrittenInPost = gbr.getDataset().getId().equals(df.getOwner().getId()) && delta <= (GUESTBOOK_RESPONSE_SIGNEDURL_TIMEOUT_MINUTES * 60000L); + wasWrittenInPost = gbr.getDataset().getId().equals(ds.getId()) && delta <= (GUESTBOOK_RESPONSE_SIGNEDURL_TIMEOUT_MINUTES * 60000L); } catch (NumberFormatException | DateTimeParseException ex) { throw new BadRequestException(ex.getMessage()); } @@ -2283,20 +2279,11 @@ private GuestbookResponse getGuestbookResponseFromBody(DataFile dataFile, String // checkAuthorization is a convenience method; it calls the boolean method // isAccessAuthorized(), the actual workhorse, and throws a 403 exception if not. - private void checkAuthorization(User initialUser, DataFile df) throws WebApplicationException { - User user = getRequestor(initialUser); - if (!isAccessAuthorized(user, df)) { + private void checkAuthorization(User requestUser, DataFile df) throws WebApplicationException { + if (!isAccessAuthorized(requestUser, df)) { throw new ForbiddenException(); } } - private User getRequestor(User user) { - // CompoundAuthMechanism should find the user by API Key/Token, Workflow, etc. And for SPA the Bearer Token - // For JSF check if CompoundAuthMechanism couldn't find the user then try to get it from the session - if (session!=null && user instanceof GuestUser) { - user = session.getUser(); - } - return user; - } private boolean isAccessAuthorized(User requestUser, DataFile df) { // First, check if the file belongs to a released Dataset version: diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index c1471c3f5b3..8afda000fed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -7,15 +7,27 @@ import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class SessionCookieAuthMechanism implements AuthMechanism { @Inject DataverseSession session; @Override public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { - if (FeatureFlags.API_SESSION_AUTH.enabled()) { + if (FeatureFlags.API_SESSION_AUTH.enabled() || isAccessApi(containerRequestContext)) { return session.getUser(); } return null; } + + private boolean isAccessApi(ContainerRequestContext containerRequestContext) { + if (containerRequestContext.getMethod().equals("GET")) { + Pattern pattern = Pattern.compile("/api.*/access/"); // /api/v1/access/ or /api/access/ + Matcher matcher = pattern.matcher(containerRequestContext.getUriInfo().getAbsolutePath().toString().toLowerCase()); + return matcher.find(); + } + return false; + } } From 7723ac14110531d69a356e22b51299b976b7770b Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:50:05 -0400 Subject: [PATCH 2/8] fix npe SessionCookieAuthMechanism --- .../iq/dataverse/api/auth/SessionCookieAuthMechanism.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index 8afda000fed..6db95c4e72f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -23,7 +23,7 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } private boolean isAccessApi(ContainerRequestContext containerRequestContext) { - if (containerRequestContext.getMethod().equals("GET")) { + if (containerRequestContext.getMethod() != null && containerRequestContext.getMethod().equals("GET")) { Pattern pattern = Pattern.compile("/api.*/access/"); // /api/v1/access/ or /api/access/ Matcher matcher = pattern.matcher(containerRequestContext.getUriInfo().getAbsolutePath().toString().toLowerCase()); return matcher.find(); From aa4a1c8357041328bf0d7866ecec7121414b3e05 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:57:08 -0400 Subject: [PATCH 3/8] change perm to viewUnpublishedDataset from editDataset --- src/main/java/edu/harvard/iq/dataverse/api/Access.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d29bdba9d60..7f53798555b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -2237,7 +2237,7 @@ private boolean checkGuestbookRequiredResponse(User user, UriInfo uriInfo, DataF boolean required = ds.hasEnabledGuestbook() && !ds.getEffectiveGuestbookEntryAtRequest() && !(user instanceof PrivateUrlUser); boolean wasWrittenInPost = false; if (required) { - if (user instanceof AuthenticatedUser && permissionService.userOn(user, ds).has(Permission.EditDataset)) { + if (permissionService.userOn(user, ds).has(Permission.ViewUnpublishedDataset)) { required = false; } // Check if we are downloading a thumbnail image which doesn't require a guestbook response From 5d04c58627b94cf519937940bd5e6b9ee9d3812a Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:27:41 -0400 Subject: [PATCH 4/8] refactor isAccessApi --- .../api/auth/SessionCookieAuthMechanism.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index 6db95c4e72f..dd4f6e03326 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -7,9 +7,6 @@ import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - public class SessionCookieAuthMechanism implements AuthMechanism { @Inject DataverseSession session; @@ -23,11 +20,7 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } private boolean isAccessApi(ContainerRequestContext containerRequestContext) { - if (containerRequestContext.getMethod() != null && containerRequestContext.getMethod().equals("GET")) { - Pattern pattern = Pattern.compile("/api.*/access/"); // /api/v1/access/ or /api/access/ - Matcher matcher = pattern.matcher(containerRequestContext.getUriInfo().getAbsolutePath().toString().toLowerCase()); - return matcher.find(); - } - return false; + return "GET".equalsIgnoreCase(containerRequestContext.getMethod()) + && containerRequestContext.getUriInfo().getPath().toLowerCase().startsWith("/access/"); } } From 9e953bc37732f4841bb80839481a968c2a37dabc Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:06:03 -0400 Subject: [PATCH 5/8] fix for npe in unit test --- .../java/edu/harvard/iq/dataverse/api/Access.java | 12 +++++++----- .../api/auth/SessionCookieAuthMechanism.java | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) 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 7f53798555b..f51cf395710 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -2232,21 +2232,23 @@ public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc, } private boolean checkGuestbookRequiredResponse(User user, UriInfo uriInfo, DataFile df, String gbrids) throws WebApplicationException { + // checkAuthorization must be called first to verify the user's permission to download this file // Check if guestbook response is required Dataset ds = df.getOwner(); - boolean required = ds.hasEnabledGuestbook() && !ds.getEffectiveGuestbookEntryAtRequest() && !(user instanceof PrivateUrlUser); + boolean required = ds.hasEnabledGuestbook() && !ds.getEffectiveGuestbookEntryAtRequest(); boolean wasWrittenInPost = false; if (required) { - if (permissionService.userOn(user, ds).has(Permission.ViewUnpublishedDataset)) { - required = false; + // PrivateUrlUsers are exempt from this requirement + if (user instanceof PrivateUrlUser) { + return false; } // Check if we are downloading a thumbnail image which doesn't require a guestbook response boolean imageThumb = uriInfo.getQueryParameters().containsKey("imageThumb"); if (imageThumb) { - required = false; + return false; } - if (required && gbrids != null && !gbrids.isEmpty()) { + if (gbrids != null && !gbrids.isEmpty()) { try { // verify that this id is good GuestbookResponse gbr = guestbookResponseService.findById(Long.valueOf(gbrids)); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index dd4f6e03326..59f7b69301f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -21,6 +21,7 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) private boolean isAccessApi(ContainerRequestContext containerRequestContext) { return "GET".equalsIgnoreCase(containerRequestContext.getMethod()) - && containerRequestContext.getUriInfo().getPath().toLowerCase().startsWith("/access/"); + && containerRequestContext.getUriInfo() != null + && containerRequestContext.getUriInfo().getPath().toLowerCase().startsWith("access/"); } } From 131ecb64b9eca25cf2cf0922f1470b0cfc398d9d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 5 Aug 2026 10:55:57 -0400 Subject: [PATCH 6/8] add LF fix info --- ...535-download-without-guestbook-response-for-preview-user2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md b/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md index 5c2eaa05a9b..aabee9de5f1 100644 --- a/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md +++ b/doc/release-notes/12535-download-without-guestbook-response-for-preview-user2.md @@ -1,2 +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. +Preview URL users could not download files from the dataset being previewed if a guestbook was assigned to that dataset. This is now fixed. A similar issue with Locally FAIR content is also fixed. From cf7d78c89c5f66380232be40f1bfa8f9bd14c10f Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:00:48 -0400 Subject: [PATCH 7/8] adding fixes per review comments --- src/main/java/edu/harvard/iq/dataverse/api/Access.java | 2 +- .../iq/dataverse/api/auth/SessionCookieAuthMechanism.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) 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 f51cf395710..6642c120045 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -2232,12 +2232,12 @@ public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc, } private boolean checkGuestbookRequiredResponse(User user, UriInfo uriInfo, DataFile df, String gbrids) throws WebApplicationException { - // checkAuthorization must be called first to verify the user's permission to download this file // Check if guestbook response is required Dataset ds = df.getOwner(); boolean required = ds.hasEnabledGuestbook() && !ds.getEffectiveGuestbookEntryAtRequest(); boolean wasWrittenInPost = false; if (required) { + checkAuthorization(user, df); // PrivateUrlUsers are exempt from this requirement if (user instanceof PrivateUrlUser) { return false; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index 59f7b69301f..f615d6e9ea4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -11,6 +11,8 @@ public class SessionCookieAuthMechanism implements AuthMechanism { @Inject DataverseSession session; + public static final String ACCESS_DATAFILE_PATH_PREFIX = "access/datafile/"; + @Override public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { if (FeatureFlags.API_SESSION_AUTH.enabled() || isAccessApi(containerRequestContext)) { @@ -20,8 +22,8 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } private boolean isAccessApi(ContainerRequestContext containerRequestContext) { - return "GET".equalsIgnoreCase(containerRequestContext.getMethod()) - && containerRequestContext.getUriInfo() != null - && containerRequestContext.getUriInfo().getPath().toLowerCase().startsWith("access/"); + String requestPath = containerRequestContext.getUriInfo() != null ? containerRequestContext.getUriInfo().getPath() : ""; + return ("GET".equalsIgnoreCase(containerRequestContext.getMethod()) && + requestPath.startsWith(ACCESS_DATAFILE_PATH_PREFIX)); } } From f01525d00a853181c45f6d3217c2da99832388f8 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:19:36 -0400 Subject: [PATCH 8/8] broaden access api to access/ without limiting to access/datafile --- .../iq/dataverse/api/auth/SessionCookieAuthMechanism.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java index f615d6e9ea4..d53ea7476c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java @@ -11,7 +11,7 @@ public class SessionCookieAuthMechanism implements AuthMechanism { @Inject DataverseSession session; - public static final String ACCESS_DATAFILE_PATH_PREFIX = "access/datafile/"; + public static final String ACCESS_PATH_PREFIX = "access/"; @Override public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { @@ -24,6 +24,6 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) private boolean isAccessApi(ContainerRequestContext containerRequestContext) { String requestPath = containerRequestContext.getUriInfo() != null ? containerRequestContext.getUriInfo().getPath() : ""; return ("GET".equalsIgnoreCase(containerRequestContext.getMethod()) && - requestPath.startsWith(ACCESS_DATAFILE_PATH_PREFIX)); + requestPath.startsWith(ACCESS_PATH_PREFIX)); } }