Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b4058a3
Show history of Access Requests via API
stevenwinship Dec 2, 2025
5155a1f
ui work
stevenwinship Dec 2, 2025
771a99a
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 3, 2025
bcb7c08
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 3, 2025
b7c8c74
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 4, 2025
dd64692
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 4, 2025
d2ecc5b
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 5, 2025
39ca424
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 9, 2025
a1f8175
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 12, 2025
b57b3b3
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 12, 2025
1336e91
use Pager class
stevenwinship Dec 15, 2025
3bee279
fix doc
stevenwinship Dec 15, 2025
bfd32dc
fix doc
stevenwinship Dec 15, 2025
ecdbae8
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 18, 2025
30a1df1
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 18, 2025
201f994
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 19, 2025
ea51287
Merge branch 'develop' into 8013-history-of-access-request-available-…
stevenwinship Dec 23, 2025
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,8 @@
### Feature: Extend List File Access Requests API ###

Added ability to get access request history via the `/datafile/{id}/listRequests` API. The API returns a list of users/groups where the request for access is waiting for an accept or reject. Already accepted or rejected requests are not returned.

By adding the flag 'includeHistory=true' all of the requests will be returned. Pagination is also implemented in this feature. Adding a start page parameter and max list size (`&start=0` and `&per_page=20`) can limit the amount of data being returned.

See https://guides.dataverse.org/en/latest/api/dataaccess.html#list-file-access-requests

15 changes: 15 additions & 0 deletions doc/sphinx-guides/source/api/dataaccess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,21 @@ A curl example using an ``id``::

curl -H "X-Dataverse-key:$API_TOKEN" -X GET http://$SERVER/api/access/datafile/{id}/listRequests

Query parameters have been added to retrieve the historical list of "created", "granted", and "rejected" requests:

* `includeHistory` When `true` this will force the return of all requests and not just the "created" ones.
* `start` For pagination, use this to request a specific page.
* `per_page` For pagination, use this to limit the number of items in each paged list.

.. note:: Pagination is only available when `includeHistory` is `true`

If requesting a page beyond the last page this API will return a 404 "There are no access requests for this file:..."
If requesting a page before page 1 or requesting the number of items to be 0 or less this API will ignore these parameters and return the entire list.

A curl example using an ``id``::

curl -H "X-Dataverse-key:$API_TOKEN" -X GET http://$SERVER/api/access/datafile/{id}/listRequests?includeHistory=true&start=1&per_page=20

User Has Requested Access to a File:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
37 changes: 33 additions & 4 deletions src/main/java/edu/harvard/iq/dataverse/DataFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,31 @@ public String toString() {
inverseJoinColumns = @JoinColumn(name = "authenticated_user_id"))
private List<AuthenticatedUser> fileAccessRequesters;


public List<FileAccessRequest> getFileAccessRequests(){
return fileAccessRequests;
public List<FileAccessRequest> getFileAccessRequests() {
return getFileAccessRequests(0, 0);
}


/**
* Get Requests with pagination option
* @param numResultsPerPageRequested
* @param paginationStart starts at 1
* @return
*/
public List<FileAccessRequest> getFileAccessRequests(int numResultsPerPageRequested, int paginationStart) {
if (numResultsPerPageRequested < 1 || paginationStart < 1) {
return fileAccessRequests;
} else {
int startIndex = (paginationStart - 1) * numResultsPerPageRequested;
int endIndex = startIndex + numResultsPerPageRequested;
if (startIndex >= fileAccessRequests.size()) {
return List.of();
} else if (endIndex > fileAccessRequests.size()) {
endIndex = fileAccessRequests.size();
}
return fileAccessRequests.subList(startIndex, endIndex);
}
}

public List<FileAccessRequest> getFileAccessRequests(FileAccessRequest.RequestState state){
return fileAccessRequests.stream().filter(far -> far.getState() == state).collect(Collectors.toList());
}
Expand Down Expand Up @@ -828,6 +848,15 @@ public void addFileAccessRequest(FileAccessRequest request) {
this.fileAccessRequests.add(request);
}

public List<FileAccessRequest> getAccessRequestsForAssignee(RoleAssignee roleAssignee) {
if (this.fileAccessRequests == null) {
return null;
}

return this.fileAccessRequests.stream()
.filter(fileAccessRequest -> fileAccessRequest.getRequester().equals(roleAssignee)).toList();
}

public FileAccessRequest getAccessRequestForAssignee(RoleAssignee roleAssignee) {
if (this.fileAccessRequests == null) {
return null;
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Date;

import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.util.BundleUtil;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand Down Expand Up @@ -143,6 +144,20 @@ public String getStateLabel() {
}
return null;
}

// For use by UI to allow for internationalization
public String getStateLabelNationalized() {
if(isStateCreated()) {
return BundleUtil.getStringFromBundle("permission.fileAccess.created");
}
if(isStateGranted()) {
return BundleUtil.getStringFromBundle("permission.fileAccess.granted");
}
if(isStateRejected()) {
return BundleUtil.getStringFromBundle("permission.fileAccess.rejected");
}
return null;
}

public void setStateCreated() {
this.requestState = RequestState.CREATED;
Expand Down Expand Up @@ -197,4 +212,4 @@ public boolean equals(Object object) {
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ public boolean isShowDeleted() {
public void setShowDeleted(boolean showDeleted) {
this.showDeleted = showDeleted;
}
private boolean showHistory = false;

public boolean isShowHistory() {
return showHistory;
}

public void setShowHistory(boolean showHistory) {
this.showHistory = showHistory;
}

public Dataset getDataset() {
return dataset;
Expand Down Expand Up @@ -143,6 +152,13 @@ public void showDeletedCheckboxChange() {
}

}
private boolean backingShowHistory = false;
public void showHistoryCheckboxChange() {
if (backingShowHistory != showHistory) {
initMaps();
backingShowHistory = showHistory;
}
}

public String init() {
if (dataset.getId() != null) {
Expand Down Expand Up @@ -199,7 +215,7 @@ private void initMaps() {
fileMap.put(file, raList);

// populate the file access requests map
for (FileAccessRequest fileAccessRequest : file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED)) {
for (FileAccessRequest fileAccessRequest : !showHistory ? file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) : file.getFileAccessRequests()) {
List<FileAccessRequest> fileAccessRequestList = fileAccessRequestMap.get(fileAccessRequest.getRequester());
if (fileAccessRequestList == null) {
fileAccessRequestList = new ArrayList<>();
Expand Down Expand Up @@ -250,6 +266,21 @@ public String formatAccessRequestTimestamp(List<FileAccessRequest> fileAccessReq
return Util.getDateTimeFormat().format(date);
}

public String getAccessRequestStates(List<FileAccessRequest> fileAccessRequests) {
String result = "";
if (fileAccessRequests != null) {
Map<String, Long> items = fileAccessRequests.stream()
.sorted(Comparator.comparing(FileAccessRequest::getState))
.collect(Collectors.groupingBy(
FileAccessRequest::getStateLabelNationalized,
Collectors.counting()));

result = items.entrySet().stream().map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining(", ", "[ ", " ]"));
}
return result;
}

private void addFileToRoleAssignee(RoleAssignment assignment, boolean fileDeleted) {
RoleAssignee ra = roleAssigneeService.getRoleAssignee(assignment.getAssigneeIdentifier());
List<RoleAssignmentRow> assignments = roleAssigneeMap.get(ra);
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/edu/harvard/iq/dataverse/api/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import edu.harvard.iq.dataverse.export.DDIExportServiceBean;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
import edu.harvard.iq.dataverse.mydata.Pager;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.FileUtil;
Expand Down Expand Up @@ -1467,8 +1468,11 @@ public Response requestFileAccess(@Context ContainerRequestContext crc, @PathPar
@GET
@AuthRequired
@Path("/datafile/{id}/listRequests")
public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @Context HttpHeaders headers) {

public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId,
@QueryParam("includeHistory") boolean includeHistory,
@QueryParam("per_page") final int numResultsPerPageRequested,
@QueryParam("start") final int paginationStart,
@Context HttpHeaders headers) {
DataverseRequest dataverseRequest;

DataFile dataFile;
Expand All @@ -1489,7 +1493,8 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa
return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions"));
}

List<FileAccessRequest> requests = dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED);
List<FileAccessRequest> requests = !includeHistory ? dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) :
dataFile.getFileAccessRequests(numResultsPerPageRequested, paginationStart);

if (requests == null || requests.isEmpty()) {
List<String> args = Arrays.asList(dataFile.getDisplayName());
Expand All @@ -1499,7 +1504,21 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa
JsonArrayBuilder userArray = Json.createArrayBuilder();

for (FileAccessRequest fileAccessRequest : requests) {
userArray.add(json(fileAccessRequest.getRequester()));
userArray.add(json(fileAccessRequest));
}

// Check for pagination request
if (includeHistory && numResultsPerPageRequested > 0 && paginationStart > 0) {
JsonObjectBuilder builder = Json.createObjectBuilder()
.add("status", ApiConstants.STATUS_OK)
.add("data", userArray);

Pager pager = new Pager(dataFile.getFileAccessRequests().size(), numResultsPerPageRequested, paginationStart);
builder.add("pagination", pager.asJsonObjectBuilder());

return Response.ok( builder.build() )
.type(MediaType.APPLICATION_JSON)
.build();
}

return ok(userArray);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ public static JsonObjectBuilder json(AuthenticatedUser authenticatedUser) {
.add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId());
return builder;
}
public static JsonObjectBuilder json(FileAccessRequest fileAccessRequest) {
JsonObjectBuilder builder = json(fileAccessRequest.getRequester())
.add("requestState", fileAccessRequest.getStateLabel());
return builder;
}

public static JsonArrayBuilder jsonRoleAssignments(List<RoleAssignment> roleAssignments) {
JsonArrayBuilder bld = Json.createArrayBuilder();
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/propertyFiles/Bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,7 @@ dataverse.permissionsFiles.files.invalidMsg=There are no restricted files in thi
dataverse.permissionsFiles.files.requested=Requested Files
dataverse.permissionsFiles.files.selected=Selecting {0} of {1} {2}
dataverse.permissionsFiles.files.includeDeleted=Include Deleted Files
dataverse.permissionsFiles.files.showHistory=Show Historical Requests
dataverse.permissionsFiles.files.draftUnpublished=Draft/Unpublished
dataverse.permissionsFiles.viewRemoveDialog.header=File Access
dataverse.permissionsFiles.viewRemoveDialog.removeBtn=Remove Access
Expand Down Expand Up @@ -2759,6 +2760,9 @@ permission.roleNotAbleToBeRemoved=The role assignment was not able to be removed
permission.fileAccessGranted=File Access request by {0} was granted.
permission.fileAccessRejected=File Access request by {0} was rejected.
permission.roleNotAbleToBeAssigned=The role was not able to be assigned.
permission.fileAccess.created=Requested
permission.fileAccess.granted=Granted
permission.fileAccess.rejected=Rejected

#ManageGroupsPage.java
dataverse.manageGroups.create.success=Successfully created group {0}. Refresh to update your page.
Expand Down
Loading