Skip to content

Commit

Permalink
Merge pull request IQSS#10167 from IQSS/10155-can-download-at-least-o…
Browse files Browse the repository at this point in the history
…ne-file-ds-user-perm

Add API endpoint to know if a user can download at least one file in a Dataset version
  • Loading branch information
sekmiller authored Dec 20, 2023
2 parents 8527388 + 37a61e9 commit 5cbb895
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The getCanDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created.

This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered.
18 changes: 18 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2686,6 +2686,24 @@ In particular, the user permissions that this API call checks, returned as boole
curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/userPermissions"
Know If a User Can Download at Least One File from a Dataset Version
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that permissions based on :ref:`shib-groups` are not considered.
.. code-block:: bash
export SERVER_URL=https://demo.dataverse.org
export ID=24
export VERSION=1.0
curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/versions/$VERSION/canDownloadAtLeastOneFile"
The fully expanded example above (without environment variables) looks like this:
.. code-block:: bash
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/datasets/24/versions/1.0/canDownloadAtLeastOneFile"
Files
-----
Expand Down
2 changes: 2 additions & 0 deletions doc/sphinx-guides/source/installation/shibboleth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ Rather than looking up the user's id in the ``authenticateduser`` database table

Per above, you now need to tell the user to use the password reset feature to set a password for their local account.

.. _shib-groups:

Institution-Wide Shibboleth Groups
----------------------------------

Expand Down
2 changes: 1 addition & 1 deletion docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ services:
networks:
- dataverse
volumes:
- minio_storage:/data
- ./docker-dev-volumes/minio_storage:/data
environment:
MINIO_ROOT_USER: 4cc355_k3y
MINIO_ROOT_PASSWORD: s3cr3t_4cc355_k3y
Expand Down
61 changes: 59 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;

/**
* Your one-stop-shop for deciding which user can do what action on which
Expand Down Expand Up @@ -448,8 +451,9 @@ private boolean isPublicallyDownloadable(DvObject dvo) {

if (!df.isRestricted()) {
if (df.getOwner().getReleasedVersion() != null) {
if (df.getOwner().getReleasedVersion().getFileMetadatas() != null) {
for (FileMetadata fm : df.getOwner().getReleasedVersion().getFileMetadatas()) {
List<FileMetadata> fileMetadatas = df.getOwner().getReleasedVersion().getFileMetadatas();
if (fileMetadatas != null) {
for (FileMetadata fm : fileMetadatas) {
if (df.equals(fm.getDataFile())) {
return true;
}
Expand Down Expand Up @@ -837,4 +841,57 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio
return false;
}

/**
* Checks if a DataverseRequest can download at least one file of the target DatasetVersion.
*
* @param dataverseRequest DataverseRequest to check
* @param datasetVersion DatasetVersion to check
* @return boolean indicating whether the user can download at least one file or not
*/
public boolean canDownloadAtLeastOneFile(DataverseRequest dataverseRequest, DatasetVersion datasetVersion) {
if (hasUnrestrictedReleasedFiles(datasetVersion)) {
return true;
}
List<FileMetadata> fileMetadatas = datasetVersion.getFileMetadatas();
for (FileMetadata fileMetadata : fileMetadatas) {
DataFile dataFile = fileMetadata.getDataFile();
Set<RoleAssignee> roleAssignees = new HashSet<>(groupService.groupsFor(dataverseRequest, dataFile));
roleAssignees.add(dataverseRequest.getUser());
if (hasGroupPermissionsFor(roleAssignees, dataFile, EnumSet.of(Permission.DownloadFile))) {
return true;
}
}
return false;
}

/**
* Checks if a DatasetVersion has unrestricted released files.
*
* This method is mostly based on {@link #isPublicallyDownloadable(DvObject)} although in this case, instead of basing
* the search on a particular file, it searches for the total number of files in the target version that are present
* in the released version.
*
* @param targetDatasetVersion DatasetVersion to check
* @return boolean indicating whether the dataset version has released files or not
*/
private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion) {
Dataset targetDataset = targetDatasetVersion.getDataset();
if (!targetDataset.isReleased()) {
return false;
}
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
Root<DatasetVersion> datasetVersionRoot = criteriaQuery.from(DatasetVersion.class);
Root<FileMetadata> fileMetadataRoot = criteriaQuery.from(FileMetadata.class);
criteriaQuery
.select(criteriaBuilder.count(fileMetadataRoot))
.where(criteriaBuilder.and(
criteriaBuilder.equal(fileMetadataRoot.get("dataFile").get("restricted"), false),
criteriaBuilder.equal(datasetVersionRoot.get("dataset"), targetDataset),
criteriaBuilder.equal(datasetVersionRoot.get("versionState"), DatasetVersion.VersionState.RELEASED),
fileMetadataRoot.in(targetDatasetVersion.getFileMetadatas()),
fileMetadataRoot.in(datasetVersionRoot.get("fileMetadatas"))));
Long result = em.createQuery(criteriaQuery).getSingleResult();
return result > 0;
}
}
15 changes: 15 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -4638,4 +4638,19 @@ public Response getUserPermissionsOnDataset(@Context ContainerRequestContext crc
jsonObjectBuilder.add("canDeleteDatasetDraft", permissionService.userOn(requestUser, dataset).has(Permission.DeleteDatasetDraft));
return ok(jsonObjectBuilder);
}

@GET
@AuthRequired
@Path("{id}/versions/{versionId}/canDownloadAtLeastOneFile")
public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext crc,
@PathParam("id") String datasetId,
@PathParam("versionId") String versionId,
@QueryParam("includeDeaccessioned") boolean includeDeaccessioned,
@Context UriInfo uriInfo,
@Context HttpHeaders headers) {
return response(req -> {
DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, includeDeaccessioned);
return ok(permissionService.canDownloadAtLeastOneFile(req, datasetVersion));
}, getRequestUser(crc));
}
}
130 changes: 119 additions & 11 deletions src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@
import javax.xml.stream.XMLStreamReader;

import static java.lang.Thread.sleep;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.hamcrest.CoreMatchers;

Expand All @@ -94,11 +93,7 @@
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.contains;

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.*;


public class DatasetsIT {
Expand Down Expand Up @@ -138,7 +133,7 @@ public static void setUpClass() {
.statusCode(200);
*/
}


@AfterAll
public static void afterClass() {
Expand Down Expand Up @@ -4180,7 +4175,7 @@ public void testGetUserPermissionsOnDataset() {
Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getUserPermissionsOnDataset("testInvalidId", apiToken);
getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode());
}

//Requires that a Globus remote store be set up as with the parameters in the GlobusOverlayAccessIOTest class
//Tests whether the API call succeeds and has some of the expected parameters
@Test
Expand All @@ -4201,13 +4196,13 @@ public void testGetGlobusUploadParameters() {
Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
createDatasetResponse.then().assertThat().statusCode(CREATED.getStatusCode());
int datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id");

Response makeSuperUser = UtilIT.makeSuperUser(username);
assertEquals(200, makeSuperUser.getStatusCode());

Response setDriver = UtilIT.setDatasetStorageDriver(datasetId, System.getProperty("dataverse.files.globusr.label"), apiToken);
assertEquals(200, setDriver.getStatusCode());

Response getUploadParams = UtilIT.getDatasetGlobusUploadParameters(datasetId, "en_us", apiToken);
assertEquals(200, getUploadParams.getStatusCode());
JsonObject data = JsonUtil.getJsonObject(getUploadParams.getBody().asString());
Expand All @@ -4229,4 +4224,117 @@ public void testGetGlobusUploadParameters() {
//Removes managed and remote Globus stores
GlobusOverlayAccessIOTest.tearDown();
}

@Test
public void testGetCanDownloadAtLeastOneFile() {
Response createUserResponse = UtilIT.createRandomUser();
createUserResponse.then().assertThat().statusCode(OK.getStatusCode());
String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse);

Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken);
createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode());
String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse);

Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
createDatasetResponse.then().assertThat().statusCode(CREATED.getStatusCode());
int datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id");
String datasetPersistentId = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId");

// Upload file
String pathToTestFile = "src/test/resources/images/coffeeshop.png";
Response uploadResponse = UtilIT.uploadFileViaNative(Integer.toString(datasetId), pathToTestFile, Json.createObjectBuilder().build(), apiToken);
uploadResponse.then().assertThat().statusCode(OK.getStatusCode());

String fileId = JsonPath.from(uploadResponse.body().asString()).getString("data.files[0].dataFile.id");

// Publish dataset version
Response publishDataverseResponse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken);
publishDataverseResponse.then().assertThat().statusCode(OK.getStatusCode());
Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken);
publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());

// Create a second user to call the getCanDownloadAtLeastOneFile method
Response createSecondUserResponse = UtilIT.createRandomUser();
createSecondUserResponse.then().assertThat().statusCode(OK.getStatusCode());
String secondUserApiToken = UtilIT.getApiTokenFromResponse(createSecondUserResponse);
String secondUserUsername = UtilIT.getUsernameFromResponse(createSecondUserResponse);

// Call when a file is released
Response canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
boolean canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertTrue(canDownloadAtLeastOneFile);

// Restrict file
Response restrictFileResponse = UtilIT.restrictFile(fileId, true, apiToken);
restrictFileResponse.then().assertThat().statusCode(OK.getStatusCode());

// Publish dataset version
publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken);
publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());

// Call when a file is restricted and the user does not have access
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertFalse(canDownloadAtLeastOneFile);

// Grant restricted file access to the user
Response grantFileAccessResponse = UtilIT.grantFileAccess(fileId, "@" + secondUserUsername, apiToken);
grantFileAccessResponse.then().assertThat().statusCode(OK.getStatusCode());

// Call when a file is restricted and the user has access
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertTrue(canDownloadAtLeastOneFile);

// Create a third user to call the getCanDownloadAtLeastOneFile method
Response createThirdUserResponse = UtilIT.createRandomUser();
createThirdUserResponse.then().assertThat().statusCode(OK.getStatusCode());
String thirdUserApiToken = UtilIT.getApiTokenFromResponse(createThirdUserResponse);
String thirdUserUsername = UtilIT.getUsernameFromResponse(createThirdUserResponse);

// Call when a file is restricted and the user does not have access
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, thirdUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertFalse(canDownloadAtLeastOneFile);

// Grant fileDownloader role on the dataset to the user
Response grantDatasetFileDownloaderRoleOnDatasetResponse = UtilIT.grantRoleOnDataset(datasetPersistentId, "fileDownloader", "@" + thirdUserUsername, apiToken);
grantDatasetFileDownloaderRoleOnDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());

// Call when a file is restricted and the user has fileDownloader role on the dataset
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, thirdUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertTrue(canDownloadAtLeastOneFile);

// Create a fourth user to call the getCanDownloadAtLeastOneFile method
Response createFourthUserResponse = UtilIT.createRandomUser();
createFourthUserResponse.then().assertThat().statusCode(OK.getStatusCode());
String fourthUserApiToken = UtilIT.getApiTokenFromResponse(createFourthUserResponse);
String fourthUserUsername = UtilIT.getUsernameFromResponse(createFourthUserResponse);

// Call when a file is restricted and the user does not have access
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, fourthUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertFalse(canDownloadAtLeastOneFile);

// Grant fileDownloader role on the collection to the user
Response grantDatasetFileDownloaderRoleOnCollectionResponse = UtilIT.grantRoleOnDataverse(dataverseAlias, "fileDownloader", "@" + fourthUserUsername, apiToken);
grantDatasetFileDownloaderRoleOnCollectionResponse.then().assertThat().statusCode(OK.getStatusCode());

// Call when a file is restricted and the user has fileDownloader role on the collection
canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, fourthUserApiToken);
canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode());
canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data");
assertTrue(canDownloadAtLeastOneFile);

// Call with invalid dataset id
Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getCanDownloadAtLeastOneFile("testInvalidId", DS_VERSION_LATEST, secondUserApiToken);
getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode());
}
}
6 changes: 6 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -3546,6 +3546,12 @@ static Response getUserPermissionsOnDataset(String datasetId, String apiToken) {
.get("/api/datasets/" + datasetId + "/userPermissions");
}

static Response getCanDownloadAtLeastOneFile(String datasetId, String versionId, String apiToken) {
return given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("/api/datasets/" + datasetId + "/versions/" + versionId + "/canDownloadAtLeastOneFile");
}

static Response createFileEmbargo(Integer datasetId, Integer fileId, String dateAvailable, String apiToken) {
JsonObjectBuilder jsonBuilder = Json.createObjectBuilder();
jsonBuilder.add("dateAvailable", dateAvailable);
Expand Down

0 comments on commit 5cbb895

Please sign in to comment.