Skip to content

Commit

Permalink
Merge pull request IQSS#11079 from IQSS/10930-marketplace-external-to…
Browse files Browse the repository at this point in the history
…ols-apis

add new non-admin APIs for external tools registration (for marketplace)
  • Loading branch information
ofahimIQSS authored Jan 3, 2025
2 parents ef1f293 + 4faadf6 commit 2c471ae
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 2 deletions.
14 changes: 14 additions & 0 deletions doc/release-notes/10930-marketplace-external-tools-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## New APIs for External Tools Registration for Marketplace

New API base path /api/externalTools created that mimics the admin APIs /api/admin/externalTools. These new add and delete apis require an authenticated superuser token.

Example:
```
API_TOKEN='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
export TOOL_ID=1
curl http://localhost:8080/api/externalTools
curl http://localhost:8080/api/externalTools/$TOOL_ID
curl -s -H "X-Dataverse-key:$API_TOKEN" -X POST -H 'Content-type: application/json' http://localhost:8080/api/externalTools --upload-file fabulousFileTool.json
curl -s -H "X-Dataverse-key:$API_TOKEN" -X DELETE http://localhost:8080/api/externalTools/$TOOL_ID
```
26 changes: 25 additions & 1 deletion doc/sphinx-guides/source/admin/external-tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ Configure the tool with the curl command below, making sure to replace the ``fab

.. code-block:: bash
curl -X POST -H 'Content-type: application/json' http://localhost:8080/api/admin/externalTools --upload-file fabulousFileTool.json
curl -X POST -H 'Content-type: application/json' http://localhost:8080/api/admin/externalTools --upload-file fabulousFileTool.json
This API is Superuser only. Note the endpoint difference (/api/externalTools instead of /api/admin/externalTools).

.. code-block:: bash
curl -s -H "X-Dataverse-key:$API_TOKEN" -X POST -H 'Content-type: application/json' http://localhost:8080/api/externalTools --upload-file fabulousFileTool.json
Listing All External Tools in a Dataverse Installation
++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand All @@ -46,6 +52,12 @@ To list all the external tools that are available in a Dataverse installation:
curl http://localhost:8080/api/admin/externalTools
This API is open to any user. Note the endpoint difference (/api/externalTools instead of /api/admin/externalTools).

.. code-block:: bash
curl http://localhost:8080/api/externalTools
Showing an External Tool in a Dataverse Installation
++++++++++++++++++++++++++++++++++++++++++++++++++++

Expand All @@ -56,6 +68,12 @@ To show one of the external tools that are available in a Dataverse installation
export TOOL_ID=1
curl http://localhost:8080/api/admin/externalTools/$TOOL_ID
This API is open to any user. Note the endpoint difference (/api/externalTools instead of /api/admin/externalTools).

.. code-block:: bash
curl http://localhost:8080/api/externalTools/$TOOL_ID
Removing an External Tool From a Dataverse Installation
+++++++++++++++++++++++++++++++++++++++++++++++++++++++

Expand All @@ -66,6 +84,12 @@ Assuming the external tool database id is "1", remove it with the following comm
export TOOL_ID=1
curl -X DELETE http://localhost:8080/api/admin/externalTools/$TOOL_ID
This API is Superuser only. Note the endpoint difference (/api/externalTools instead of /api/admin/externalTools).

.. code-block:: bash
curl -s -H "X-Dataverse-key:$API_TOKEN" -X DELETE http://localhost:8080/api/externalTools/$TOOL_ID
.. _testing-external-tools:

Testing External Tools
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/ExternalToolsApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package edu.harvard.iq.dataverse.api;

import edu.harvard.iq.dataverse.api.auth.AuthRequired;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;

@Path("externalTools")
public class ExternalToolsApi extends AbstractApiBean {

@Inject
ExternalTools externalTools;

@GET
public Response getExternalTools() {
return externalTools.getExternalTools();
}

@GET
@Path("{id}")
public Response getExternalTool(@PathParam("id") long externalToolIdFromUser) {
return externalTools.getExternalTool(externalToolIdFromUser);
}

@POST
@AuthRequired
public Response addExternalTool(@Context ContainerRequestContext crc, String manifest) {
Response notAuthorized = authorize(crc);
return notAuthorized == null ? externalTools.addExternalTool(manifest) : notAuthorized;
}

@DELETE
@AuthRequired
@Path("{id}")
public Response deleteExternalTool(@Context ContainerRequestContext crc, @PathParam("id") long externalToolIdFromUser) {
Response notAuthorized = authorize(crc);
return notAuthorized == null ? externalTools.deleteExternalTool(externalToolIdFromUser) : notAuthorized;
}

private Response authorize(ContainerRequestContext crc) {
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
if (!user.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
} catch (WrappedResponse ex) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
return null;
}
}
104 changes: 103 additions & 1 deletion src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import java.nio.file.Paths;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonReader;
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import static jakarta.ws.rs.core.Response.Status.CREATED;
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import static jakarta.ws.rs.core.Response.Status.OK;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
Expand All @@ -37,6 +37,108 @@ public void testGetExternalTools() {
getExternalTools.prettyPrint();
}

@Test
public void testExternalToolsNonAdminEndpoint() {
Response createUser = UtilIT.createRandomUser();
createUser.prettyPrint();
createUser.then().assertThat()
.statusCode(OK.getStatusCode());
String username = UtilIT.getUsernameFromResponse(createUser);
String apiToken = UtilIT.getApiTokenFromResponse(createUser);
UtilIT.setSuperuserStatus(username, true);

Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken);
createDataverseResponse.prettyPrint();
createDataverseResponse.then().assertThat()
.statusCode(CREATED.getStatusCode());

String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse);

Response createDataset = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
createDataset.prettyPrint();
createDataset.then().assertThat()
.statusCode(CREATED.getStatusCode());

Integer datasetId = JsonPath.from(createDataset.getBody().asString()).getInt("data.id");
String datasetPid = JsonPath.from(createDataset.getBody().asString()).getString("data.persistentId");

String toolManifest = """
{
"displayName": "Dataset Configurator",
"description": "Slices! Dices! <a href='https://docs.datasetconfigurator.com' target='_blank'>More info</a>.",
"types": [
"configure"
],
"scope": "dataset",
"toolUrl": "https://datasetconfigurator.com",
"toolParameters": {
"queryParameters": [
{
"datasetPid": "{datasetPid}"
},
{
"localeCode": "{localeCode}"
}
]
}
}
""";

Response addExternalTool = UtilIT.addExternalTool(JsonUtil.getJsonObject(toolManifest), apiToken);
addExternalTool.prettyPrint();
addExternalTool.then().assertThat()
.statusCode(OK.getStatusCode())
.body("data.displayName", CoreMatchers.equalTo("Dataset Configurator"));

Long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id");
Response getExternalToolsByDatasetId = UtilIT.getExternalToolForDatasetById(datasetId.toString(), "configure", apiToken, toolId.toString());
getExternalToolsByDatasetId.prettyPrint();
getExternalToolsByDatasetId.then().assertThat()
.body("data.displayName", CoreMatchers.equalTo("Dataset Configurator"))
.body("data.scope", CoreMatchers.equalTo("dataset"))
.body("data.types[0]", CoreMatchers.equalTo("configure"))
.body("data.toolUrlWithQueryParams", CoreMatchers.equalTo("https://datasetconfigurator.com?datasetPid=" + datasetPid))
.statusCode(OK.getStatusCode());

Response getExternalTools = UtilIT.getExternalTools(apiToken);
getExternalTools.prettyPrint();
getExternalTools.then().assertThat()
.statusCode(OK.getStatusCode());
Response getExternalTool = UtilIT.getExternalTool(toolId, apiToken);
getExternalTool.prettyPrint();
getExternalTool.then().assertThat()
.statusCode(OK.getStatusCode());

// non superuser can only view tools
UtilIT.setSuperuserStatus(username, false);
getExternalTools = UtilIT.getExternalTools(apiToken);
getExternalTools.then().assertThat()
.statusCode(OK.getStatusCode());
getExternalToolsByDatasetId = UtilIT.getExternalToolForDatasetById(datasetId.toString(), "configure", apiToken, toolId.toString());
getExternalToolsByDatasetId.prettyPrint();
getExternalToolsByDatasetId.then().assertThat()
.statusCode(OK.getStatusCode());

//Add by non-superuser will fail
addExternalTool = UtilIT.addExternalTool(JsonUtil.getJsonObject(toolManifest), apiToken);
addExternalTool.then().assertThat()
.statusCode(FORBIDDEN.getStatusCode())
.body("message", CoreMatchers.equalTo("Superusers only."));

//Delete by non-superuser will fail
Response deleteExternalTool = UtilIT.deleteExternalTool(toolId, apiToken);
deleteExternalTool.then().assertThat()
.statusCode(FORBIDDEN.getStatusCode())
.body("message", CoreMatchers.equalTo("Superusers only."));

//Delete the tool added by this test...
UtilIT.setSuperuserStatus(username, true);
deleteExternalTool = UtilIT.deleteExternalTool(toolId, apiToken);
deleteExternalTool.prettyPrint();
deleteExternalTool.then().assertThat()
.statusCode(OK.getStatusCode());
}

@Test
public void testFileLevelTool1() {

Expand Down
36 changes: 36 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 @@ -2539,6 +2539,42 @@ static Response deleteExternalTool(long externalToolid) {
.delete("/api/admin/externalTools/" + externalToolid);
}

// ExternalTools with token
static Response getExternalTools(String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification.get("/api/externalTools");
}

static Response getExternalTool(long id, String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification.get("/api/externalTools/" + id);
}

static Response addExternalTool(JsonObject jsonObject, String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification
.body(jsonObject.toString())
.contentType(ContentType.JSON)
.post("/api/externalTools");
}

static Response deleteExternalTool(long externalToolid, String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification.delete("/api/externalTools/" + externalToolid);
}

static Response getExternalToolsForDataset(String idOrPersistentIdOfDataset, String type, String apiToken) {
String idInPath = idOrPersistentIdOfDataset; // Assume it's a number.
String optionalQueryParam = ""; // If idOrPersistentId is a number we'll just put it in the path.
Expand Down

0 comments on commit 2c471ae

Please sign in to comment.