Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new non-admin APIs for external tools registration (for marketplace) #11079

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -2538,6 +2538,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
Loading