Skip to content

Commit

Permalink
Merge pull request #10023 from IQSS/10001-dataset-api-user-permissions
Browse files Browse the repository at this point in the history
Extend the API to get Dataset user permissions, new File user permissions, and deaccessioned support for getDatasetVersion endpoint
  • Loading branch information
kcondon authored Oct 20, 2023
2 parents ab231ff + 5cd6679 commit 0af5caf
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 36 deletions.
13 changes: 13 additions & 0 deletions doc/release-notes/10001-datasets-files-api-user-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- New query parameter `includeDeaccessioned` added to the getVersion endpoint (/api/datasets/{id}/versions/{versionId}) to consider deaccessioned versions when searching for versions.


- New endpoint to get user permissions on a dataset (/api/datasets/{id}/userPermissions). In particular, the user permissions that this API call checks, returned as booleans, are the following:

- Can view the unpublished dataset
- Can edit the dataset
- Can publish the dataset
- Can manage the dataset permissions
- Can delete the dataset draft


- New permission check "canManageFilePermissions" added to the existing endpoint for getting user permissions on a file (/api/access/datafile/{id}/userPermissions).
1 change: 1 addition & 0 deletions doc/sphinx-guides/source/api/dataaccess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ This method returns the permissions that the calling user has on a particular fi
In particular, the user permissions that this method checks, returned as booleans, are the following:

* Can download the file
* Can manage the file permissions
* Can edit the file owner dataset

A curl example using an ``id``::
Expand Down
30 changes: 30 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,16 @@ The fully expanded example above (without environment variables) looks like this
The optional ``includeFiles`` parameter specifies whether the files should be listed in the output (defaults to ``true``). Note that a separate ``/files`` API can be used for listing the files, or a subset thereof in a given version.


By default, deaccessioned dataset versions are not included in the search when applying the :latest or :latest-published identifiers. Additionally, when filtering by a specific version tag, you will get a "not found" error if the version is deaccessioned and you do not enable the ``includeDeaccessioned`` option described below.

If you want to include deaccessioned dataset versions, you must set ``includeDeaccessioned`` query parameter to ``true``.

Usage example:

.. code-block:: bash
curl "https://demo.dataverse.org/api/datasets/24/versions/1.0?includeDeaccessioned=true"
.. _export-dataset-metadata-api:

Export Metadata of a Dataset in Various Formats
Expand Down Expand Up @@ -2558,6 +2568,26 @@ The API can also be used to reset the dataset to use the default/inherited value
curl -X DELETE -H "X-Dataverse-key:$API_TOKEN" -H Content-type:application/json "$SERVER_URL/api/datasets/:persistentId/guestbookEntryAtRequest?persistentId=$PERSISTENT_IDENTIFIER"
Get User Permissions on a Dataset
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This API call returns the permissions that the calling user has on a particular dataset.
In particular, the user permissions that this API call checks, returned as booleans, are the following:
* Can view the unpublished dataset
* Can edit the dataset
* Can publish the dataset
* Can manage the dataset permissions
* Can delete the dataset draft
.. code-block:: bash
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=24
curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/userPermissions"
Files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,15 +645,4 @@ public String getDirectStorageLocatrion(String storageLocation) {

return null;
}

/**
* Checks if the DataverseRequest, which contains IP Groups, has permission to download the file
*
* @param dataverseRequest the DataverseRequest
* @param dataFile the DataFile to check permissions
* @return boolean
*/
public boolean canDownloadFile(DataverseRequest dataverseRequest, DataFile dataFile) {
return permissionService.requestOn(dataverseRequest, dataFile).has(Permission.DownloadFile);
}
}
3 changes: 2 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,8 @@ public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc, @
}
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
User requestUser = getRequestUser(crc);
jsonObjectBuilder.add("canDownloadFile", fileDownloadService.canDownloadFile(createDataverseRequest(requestUser), dataFile));
jsonObjectBuilder.add("canDownloadFile", permissionService.userOn(requestUser, dataFile).has(Permission.DownloadFile));
jsonObjectBuilder.add("canManageFilePermissions", permissionService.userOn(requestUser, dataFile).has(Permission.ManageFilePermissions));
jsonObjectBuilder.add("canEditOwnerDataset", permissionService.userOn(requestUser, dataFile.getOwner()).has(Permission.EditDataset));
return ok(jsonObjectBuilder);
}
Expand Down
38 changes: 32 additions & 6 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,9 @@ public Response useDefaultCitationDate(@Context ContainerRequestContext crc, @Pa
@AuthRequired
@Path("{id}/versions")
public Response listVersions(@Context ContainerRequestContext crc, @PathParam("id") String id, @QueryParam("includeFiles") Boolean includeFiles, @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset) {

return response( req -> {
Dataset dataset = findDatasetOrDie(id);
Dataset dataset = findDatasetOrDie(id);

return ok( execCommand( new ListVersionsCommand(req, dataset, offset, limit, (includeFiles == null ? true : includeFiles)) )
.stream()
Expand All @@ -486,14 +486,20 @@ public Response listVersions(@Context ContainerRequestContext crc, @PathParam("i
@GET
@AuthRequired
@Path("{id}/versions/{versionId}")
public Response getVersion(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @QueryParam("includeFiles") Boolean includeFiles, @Context UriInfo uriInfo, @Context HttpHeaders headers) {
public Response getVersion(@Context ContainerRequestContext crc,
@PathParam("id") String datasetId,
@PathParam("versionId") String versionId,
@QueryParam("includeFiles") Boolean includeFiles,
@QueryParam("includeDeaccessioned") boolean includeDeaccessioned,
@Context UriInfo uriInfo,
@Context HttpHeaders headers) {
return response( req -> {
DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers);
DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, includeDeaccessioned);

if (dsv == null || dsv.getId() == null) {
return notFound("Dataset version not found");
}

if (includeFiles == null ? true : includeFiles) {
dsv = datasetversionService.findDeep(dsv.getId());
}
Expand Down Expand Up @@ -4112,4 +4118,24 @@ public Response resetGuestbookEntryAtRequest(@Context ContainerRequestContext cr
datasetService.merge(dataset);
return ok("Guestbook Entry At Request reset to default: " + dataset.getEffectiveGuestbookEntryAtRequest());
}

@GET
@AuthRequired
@Path("{id}/userPermissions")
public Response getUserPermissionsOnDataset(@Context ContainerRequestContext crc, @PathParam("id") String datasetId) {
Dataset dataset;
try {
dataset = findDatasetOrDie(datasetId);
} catch (WrappedResponse wr) {
return wr.getResponse();
}
User requestUser = getRequestUser(crc);
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
jsonObjectBuilder.add("canViewUnpublishedDataset", permissionService.userOn(requestUser, dataset).has(Permission.ViewUnpublishedDataset));
jsonObjectBuilder.add("canEditDataset", permissionService.userOn(requestUser, dataset).has(Permission.EditDataset));
jsonObjectBuilder.add("canPublishDataset", permissionService.userOn(requestUser, dataset).has(Permission.PublishDataset));
jsonObjectBuilder.add("canManageDatasetPermissions", permissionService.userOn(requestUser, dataset).has(Permission.ManageDatasetPermissions));
jsonObjectBuilder.add("canDeleteDatasetDraft", permissionService.userOn(requestUser, dataset).has(Permission.DeleteDatasetDraft));
return ok(jsonObjectBuilder);
}
}
2 changes: 2 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/AccessIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ public void testGetUserPermissionsOnFile() {
assertTrue(canDownloadFile);
boolean canEditOwnerDataset = JsonPath.from(getUserPermissionsOnFileResponse.body().asString()).getBoolean("data.canEditOwnerDataset");
assertTrue(canEditOwnerDataset);
boolean canManageFilePermissions = JsonPath.from(getUserPermissionsOnFileResponse.body().asString()).getBoolean("data.canManageFilePermissions");
assertTrue(canManageFilePermissions);

// Call with invalid file id
Response getUserPermissionsOnFileInvalidIdResponse = UtilIT.getUserPermissionsOnFile("testInvalidId", apiToken);
Expand Down
47 changes: 46 additions & 1 deletion src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,18 @@ public void testCreatePublishDestroyDataset() {
}
assertEquals(datasetPersistentId, XmlPath.from(exportDatasetAsDdi.body().asString()).getString("codeBook.docDscr.citation.titlStmt.IDNo"));

// Test includeDeaccessioned option
Response deaccessionDatasetResponse = UtilIT.deaccessionDataset(datasetId, DS_VERSION_LATEST_PUBLISHED, "Test deaccession reason.", null, apiToken);
deaccessionDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());

// includeDeaccessioned false
getDatasetVersion = UtilIT.getDatasetVersion(datasetPersistentId, DS_VERSION_LATEST_PUBLISHED, apiToken, false, false);
getDatasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode());

// includeDeaccessioned true
getDatasetVersion = UtilIT.getDatasetVersion(datasetPersistentId, DS_VERSION_LATEST_PUBLISHED, apiToken, false, true);
getDatasetVersion.then().assertThat().statusCode(OK.getStatusCode());

Response deleteDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken);
deleteDatasetResponse.prettyPrint();
assertEquals(200, deleteDatasetResponse.getStatusCode());
Expand Down Expand Up @@ -603,7 +615,7 @@ public void testDatasetVersionsAPI() {
// Now check that the file is NOT shown, when we ask the versions api to
// skip files:
boolean skipFiles = true;
unpublishedDraft = UtilIT.getDatasetVersion(datasetPid, ":draft", apiToken, skipFiles);
unpublishedDraft = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken, skipFiles, false);
unpublishedDraft.prettyPrint();
unpublishedDraft.then().assertThat()
.body("data.files", equalTo(null))
Expand Down Expand Up @@ -4057,4 +4069,37 @@ public void getDownloadSize() throws IOException, InterruptedException {
getVersionFileCountsGuestUserResponse = UtilIT.getDownloadSize(datasetId, "1.0", null, null, null, null, null, DatasetVersionFilesServiceBean.FileDownloadSizeMode.All.toString(), true, null);
getVersionFileCountsGuestUserResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode());
}

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

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");

// Call with valid dataset id
Response getUserPermissionsOnDatasetResponse = UtilIT.getUserPermissionsOnDataset(Integer.toString(datasetId), apiToken);
getUserPermissionsOnDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());
boolean canViewUnpublishedDataset = JsonPath.from(getUserPermissionsOnDatasetResponse.body().asString()).getBoolean("data.canViewUnpublishedDataset");
assertTrue(canViewUnpublishedDataset);
boolean canEditDataset = JsonPath.from(getUserPermissionsOnDatasetResponse.body().asString()).getBoolean("data.canEditDataset");
assertTrue(canEditDataset);
boolean canPublishDataset = JsonPath.from(getUserPermissionsOnDatasetResponse.body().asString()).getBoolean("data.canPublishDataset");
assertTrue(canPublishDataset);
boolean canManageDatasetPermissions = JsonPath.from(getUserPermissionsOnDatasetResponse.body().asString()).getBoolean("data.canManageDatasetPermissions");
assertTrue(canManageDatasetPermissions);
boolean canDeleteDatasetDraft = JsonPath.from(getUserPermissionsOnDatasetResponse.body().asString()).getBoolean("data.canDeleteDatasetDraft");
assertTrue(canDeleteDatasetDraft);

// Call with invalid dataset id
Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getUserPermissionsOnDataset("testInvalidId", apiToken);
getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode());
}
}
41 changes: 24 additions & 17 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ public static Response createRandomUser() {

return createRandomUser("user");
}

/**
* A convenience method for creating a random test user, when all you need
* is the api token.
* A convenience method for creating a random test user, when all you need
* is the api token.
* @return apiToken
*/
public static String createRandomUserGetToken(){
Response createUser = createRandomUser();
Response createUser = createRandomUser();
return getApiTokenFromResponse(createUser);
}

Expand Down Expand Up @@ -377,15 +377,15 @@ static Response createRandomDataverse(String apiToken) {
String category = null;
return createDataverse(alias, category, apiToken);
}

/**
* A convenience method for creating a random collection and getting its
* alias in one step.
* A convenience method for creating a random collection and getting its
* alias in one step.
* @param apiToken
* @return alias
*/
static String createRandomCollectionGetAlias(String apiToken){

Response createCollectionResponse = createRandomDataverse(apiToken);
//createDataverseResponse.prettyPrint();
createCollectionResponse.then().assertThat().statusCode(CREATED.getStatusCode());
Expand Down Expand Up @@ -1434,15 +1434,16 @@ static Response nativeGetUsingPersistentId(String persistentId, String apiToken)
}

static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken) {
return getDatasetVersion(persistentId, versionNumber, apiToken, false);
return getDatasetVersion(persistentId, versionNumber, apiToken, false, false);
}
static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, boolean skipFiles) {

static Response getDatasetVersion(String persistentId, String versionNumber, String apiToken, boolean skipFiles, boolean includeDeaccessioned) {
return given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("/api/datasets/:persistentId/versions/"
+ versionNumber
+ "?persistentId="
.queryParam("includeDeaccessioned", includeDeaccessioned)
.get("/api/datasets/:persistentId/versions/"
+ versionNumber
+ "?persistentId="
+ persistentId
+ (skipFiles ? "&includeFiles=false" : ""));
}
Expand Down Expand Up @@ -1808,15 +1809,15 @@ static Response removeDatasetThumbnail(String datasetPersistentId, String apiTok
static Response getDatasetVersions(String idOrPersistentId, String apiToken) {
return getDatasetVersions(idOrPersistentId, apiToken, false);
}

static Response getDatasetVersions(String idOrPersistentId, String apiToken, boolean skipFiles) {
return getDatasetVersions(idOrPersistentId, apiToken, null, null, skipFiles);
}

static Response getDatasetVersions(String idOrPersistentId, String apiToken, Integer offset, Integer limit) {
return getDatasetVersions(idOrPersistentId, apiToken, offset, limit, false);
}

static Response getDatasetVersions(String idOrPersistentId, String apiToken, Integer offset, Integer limit, boolean skipFiles) {
logger.info("Getting Dataset Versions");
String idInPath = idOrPersistentId; // Assume it's a number.
Expand Down Expand Up @@ -3434,6 +3435,12 @@ static Response getUserPermissionsOnFile(String dataFileId, String apiToken) {
.get("/api/access/datafile/" + dataFileId + "/userPermissions");
}

static Response getUserPermissionsOnDataset(String datasetId, String apiToken) {
return given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("/api/datasets/" + datasetId + "/userPermissions");
}

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 0af5caf

Please sign in to comment.