Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
adb81c2
dataset api call
qqmyers Aug 21, 2025
0e97a19
file-level call
qqmyers Aug 21, 2025
9d9d45d
add retention period check to match apis
qqmyers Aug 21, 2025
73243fe
docs
qqmyers Aug 21, 2025
fc6e66e
IT tests of dataset and file apis
qqmyers Aug 21, 2025
1a52520
release note
qqmyers Aug 21, 2025
484e47e
fix title underline
qqmyers Aug 21, 2025
a9b9f35
missing blank line
qqmyers Aug 21, 2025
d3474a4
cleanup error responses per ai review
qqmyers Aug 22, 2025
f49527b
use this file as a test
qqmyers Aug 22, 2025
713fe81
handle no params
qqmyers Aug 22, 2025
b2788de
Fix returns
qqmyers Aug 22, 2025
54dabdc
don't assume ids are first
qqmyers Aug 22, 2025
0a076aa
only callback on toolurl
qqmyers Aug 25, 2025
705fdab
update apitoken logic
qqmyers Aug 25, 2025
237c7c8
use displayName
qqmyers Aug 25, 2025
1514949
delete /api/admin/test but add some TODOs #11760
pdurbin Sep 2, 2025
520b727
Merge remote-tracking branch 'IQSS/develop' into SPA-_api_to_getToolL…
qqmyers Sep 3, 2025
01c7140
add meetsReqs
qqmyers Sep 3, 2025
65231d6
doc changes per review
qqmyers Sep 3, 2025
e8f8aa7
Apply suggestions from code review
qqmyers Sep 3, 2025
e16291c
Merge branch 'SPA-_api_to_getToolLaunchUrl' of https://github.com/Glo…
qqmyers Sep 3, 2025
2f9940f
add requirements to etools apis and fix json formatting
qqmyers Sep 3, 2025
4c10b8a
Merge pull request #44 from IQSS/11760-rm-api-admin-test
qqmyers Sep 3, 2025
4b777fb
Merge branch 'SPA-_api_to_getToolLaunchUrl' of https://github.com/Glo…
qqmyers Sep 3, 2025
dbd7f8e
update tests
qqmyers Sep 3, 2025
5735be7
add note
qqmyers Sep 3, 2025
c799b66
fix test
qqmyers Sep 4, 2025
653be76
add user who can't see draft dataset
qqmyers Sep 4, 2025
932b014
fix path for error msg
qqmyers Sep 4, 2025
6f8ee1b
typo - need user 2
qqmyers Sep 4, 2025
2b9bab9
typo
pdurbin Sep 8, 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
10 changes: 10 additions & 0 deletions doc/release-notes/11760-tool url apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
New API calls have been added to retrieve the URLs needed to launch external tools on specific datasets and files:

/api/datasets/$DATASET_ID/externalTool/$TOOL_ID/toolUrl
and
/api/files/$FILE_ID/externalTool/$TOOL_ID/toolUrl

If the dataset/file is not public, the caller must authenticate and have permission to view the dataset/file. In such cases, the generated URL will include a callback token containing a signed URL the tool can use to retrieve all the parameters it is configured for.

Backward incompatibility:
The responses from the GET /api/externalTools and /api/externalTools/{id} are now formatted as JSON (previously the toolParameters and allowedApiCalls were JSON serialized as strings) and any confiugured "requirements" are included.
1 change: 1 addition & 0 deletions doc/sphinx-guides/source/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ v6.8
- For POST /api/files/{id}/metadata passing an empty string ("description":"") or array ("categories":[]) will no longer be ignored. Empty fields will now clear out the values in the file's metadata. To ignore the fields simply do not include them in the JSON string.
- For PUT /api/datasets/{id}/editMetadata the query parameter "sourceInternalVersionNumber" has been removed and replaced with "sourceLastUpdateTime" to verify that the data being edited hasn't been modified and isn't stale.
- For GET /api/dataverses/$dataverse-alias/links the Json response has changed breaking the backward compatibility of the API.
- For GET /api/externalTools and /api/externalTools/{id} the responses are now formatted as JSON (previously the toolParameters and allowedApiCalls were a JSON object and array (respectively) that were serialized as JSON strings) and any configured "requirements" are included.

v6.7
----
Expand Down
130 changes: 129 additions & 1 deletion doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3442,10 +3442,62 @@ Archiving is an optional feature that may be configured for a Dataverse installa

curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE "$SERVER_URL/api/datasets/:persistentId/$VERSION/archivalStatus?persistentId=$PERSISTENT_IDENTIFIER"

Get Dataset External Tool URL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This API call generates a URL for accessing an external tool (see :doc:`/installation/external-tools`) that operates at the dataset level. The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration.

Authentication is required for draft or deaccessioned datasets and the user must have ViewUnpublishedDataset permission.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/7U7YBV
export TOOL_ID=42

curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/datasets/:persistentId/externalTool/$TOOL_ID/toolUrl?persistentId=$PERSISTENT_IDENTIFIER" \
-H "Content-Type: application/json" \
-d '{"preview": false, "locale": "en"}'

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/datasets/:persistentId/externalTool/42/toolUrl?persistentId=doi:10.5072/FK2/7U7YBV" \
-H "Content-Type: application/json" \
-d '{"preview": false, "locale": "en"}'

The JSON request body accepts the following optional parameters:

- ``preview``: boolean flag to indicate if the tool should run in preview mode (default: false)
- ``locale``: string specifying the locale for internationalization

The response includes:

- ``toolUrl``: the URL to access the external tool
- ``toolName``: the display name of the external tool
- ``datasetId``: the ID of the dataset
- ``preview``: whether the URL is for preview mode

Example response:

.. code-block:: json

{
"status": "OK",
"data": {
"toolUrl": "https://example.com/tool?datasetId=1234&callback=ahr...",
"toolName": "My External Tool",
"datasetId": 1234,
"preview": false
}
}

Get External Tool Parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This API call is intended as a callback that can be used by :doc:`/installation/external-tools` to retrieve signed Urls necessary for their interaction with Dataverse.
This API call is intended as a callback that can be used by :doc:`/installation/external-tools` to retrieve signed URLs necessary for their interaction with Dataverse.
It can be called directly as well.

The response is a JSON object described in the :doc:`/api/external-tools` section of the API guide.
Expand Down Expand Up @@ -5281,6 +5333,82 @@ Note the optional "limit" parameter. Without it, the API will attempt to populat

By default, the admin API calls are blocked and can only be called from localhost. See more details in :ref:`:BlockedApiEndpoints <:BlockedApiEndpoints>` and :ref:`:BlockedApiPolicy <:BlockedApiPolicy>` settings in :doc:`/installation/config`.

Get File External Tool URL
~~~~~~~~~~~~~~~~~~~~~~~~~~

This API call generates a URL for accessing an external tool (see :doc:`/installation/external-tools`) that operates at the file level. The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration.

Authentication is required for draft, restricted, embargoed, or expired (retention period) files; the user must have appropriate permissions.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export FILE_ID=42
export TOOL_ID=3

curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/files/$FILE_ID/externalTool/$TOOL_ID/toolUrl" \
-H "Content-Type: application/json" \
-d '{"preview": false, "locale": "en"}'

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/files/42/externalTool/3/toolUrl" \
-H "Content-Type: application/json" \
-d '{"preview": false, "locale": "en"}'

The JSON request body accepts the following optional parameters:

- ``preview``: boolean flag to indicate if the tool should run in preview mode (default: false)
- ``locale``: string specifying the locale for internationalization

The response includes:

- ``toolUrl``: the URL to access the external tool
- ``toolName``: the display name of the external tool
- ``fileId``: the ID of the file
- ``preview``: whether the URL is for preview mode

Example response:

.. code-block:: json

{
"status": "OK",
"data": {
"toolUrl": "https://example.com/tool?fileId=42&callback=ahr...",
"toolName": "File Viewer Tool",
"fileId": 42,
"preview": false
}
}

Get File External Tool Parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This API call is intended as a callback that can be used by :doc:`/installation/external-tools` to retrieve signed URLs necessary for their interaction with Dataverse files.
It can be called directly as well.

The response is a JSON object described in the :doc:`/api/external-tools` section of the API guide.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export FILE_ID=42
export FILE_METADATA_ID=123
export TOOL_ID=3

curl -H "X-Dataverse-key: $API_TOKEN" -H "Accept:application/json" "$SERVER_URL/api/files/$FILE_ID/metadata/$FILE_METADATA_ID/toolparams/$TOOL_ID"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -H "Accept:application/json" "https://demo.dataverse.org/api/files/42/metadata/123/toolparams/3"

Get External Tool Parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
6 changes: 4 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/FilePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -1138,8 +1138,10 @@ public void setSelectedTool(ExternalTool selectedTool) {
public String preview(ExternalTool externalTool) {
ApiToken apiToken = null;
User user = session.getUser();
if (fileMetadata.getDatasetVersion().isDraft() || fileMetadata.getDatasetVersion().isDeaccessioned() || (fileMetadata.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fileMetadata))) {
apiToken=authService.getValidApiTokenForUser(user);
if (fileMetadata.getDatasetVersion().isDraft() || (fileMetadata.getDataFile().isRestricted())
|| fileMetadata.getDatasetVersion().isDeaccessioned() || (FileUtil.isActivelyEmbargoed(fileMetadata))
|| (FileUtil.isRetentionExpired(fileMetadata))) {
apiToken = authService.getValidApiTokenForUser(user);
}
if(externalTool == null){
return "";
Expand Down
123 changes: 123 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 @@ -5153,6 +5153,129 @@ private boolean isSingleVersionArchiving() {
return false;
}

/**
* API endpoint to retrieve a URL for a dataset-level external tool.
*
* This endpoint allows clients to get a URL for accessing an external tool
* that operates at the dataset level. The URL includes necessary authentication tokens and
* parameters based on the user's permissions and the tool's configuration.
*
* The endpoint accepts JSON input with optional parameters:
* - preview: boolean flag to indicate if the tool should run in preview mode (preview mode, if supported by the tool, suppresses showing metadata (i.e. item name/PID) and is intended for cases where the tool is embedded in the dataset/file page and this metadata is not needed. The current JSF UI never embeds a dataset-level tool in an iframe, so this is param is not currently useful (and may not be supported in dataset tools yet)
* - locale: string specifying the locale for internationalization
*
* The response includes:
* - toolUrl: the URL to access the external tool
* - toolName: the display name of the external tool
* - datasetId: the ID of the dataset
* - preview: whether the URL is for preview mode
*
* Authentication is required, and appropriate permissions are checked before generating the URL.
* For restricted datasets (draft or deaccessioned), the user must have ViewUnpublishedDataset permission.
*
* @param crc The container request context for authentication
* @param datasetId The ID of the dataset
* @param externalToolId The ID of the external tool
* @param jsonBody JSON containing optional parameters
* @return A Response with the external tool URL and related information
*/
@POST
@AuthRequired
@Path("{id}/externalTool/{tid}/toolUrl")
@Consumes(MediaType.APPLICATION_JSON)
public Response getDatasetExternalToolUrl(@Context ContainerRequestContext crc, @PathParam("id") String datasetId,
@PathParam("tid") long externalToolId, String jsonBody) {

boolean preview = false;
String locale = null;

// Parse request body for parameters
if (StringUtils.isNotBlank(jsonBody)) {
try {
jakarta.json.JsonObject jsonObject = JsonUtil.getJsonObject(jsonBody);
if (jsonObject.containsKey("preview")) {
preview = jsonObject.getBoolean("preview");
}
if (jsonObject.containsKey("locale")) {
locale = jsonObject.getString("locale");
}
} catch (JsonParsingException | NullPointerException e) {
logger.warning("Error parsing JSON: " + e.getMessage());
// Return an error response for malformed JSON
return error(Response.Status.BAD_REQUEST, "Invalid JSON format in request body");
}
}

try {
// Find the dataset
Dataset dataset;
try {
dataset = findDatasetOrDie(datasetId);
} catch (WrappedResponse ex) {
return notFound("Dataset not found for given id: " + datasetId);
}

// Find the external tool
ExternalTool externalTool = externalToolService.findById(externalToolId);
if (externalTool == null) {
return error(BAD_REQUEST, "External tool not found with id: " + externalToolId);
}

// Check if the tool has dataset scope
if (!ExternalTool.Scope.DATASET.equals(externalTool.getScope())) {
return error(BAD_REQUEST, "External tool does not have dataset scope.");
}

// Get the current user and create a request object
User user = getRequestUser(crc);
DataverseRequest req = createDataverseRequest(user);

// Get the latest dataset version
DatasetVersion datasetVersion = dataset.getLatestVersion();
if (datasetVersion == null) {
return error(BAD_REQUEST, "Dataset version not found.");
}

// Check if the dataset is restricted or draft
boolean isRestricted = datasetVersion.isDraft() || datasetVersion.isDeaccessioned();

// Check if user has permission to access the dataset if it's restricted
if (isRestricted) {
boolean hasPermission = permissionSvc.requestOn(req, dataset).has(Permission.ViewUnpublishedDataset);
if (!hasPermission) {
return error(Response.Status.FORBIDDEN,
"You do not have permission to access this dataset with the requested external tool.");
}
}

// Determine if we need an API token for authentication
ApiToken apiToken = null;
if (user.isAuthenticated() && isRestricted) {
apiToken = authSvc.getValidApiTokenForUser(user);
}

// Create the external tool handler
ExternalToolHandler externalToolHandler = new ExternalToolHandler(externalTool, dataset, apiToken, locale);

// Get the tool URL
String toolUrl;
if (preview) {
toolUrl = externalToolHandler.getToolUrlForPreviewMode();
} else {
toolUrl = externalToolHandler.getToolUrlWithQueryParams();
}

// Return the URL in a JSON response
return ok(Json.createObjectBuilder().add("toolUrl", toolUrl).add("displayName", externalTool.getDisplayName())
.add("datasetId", dataset.getId()).add("preview", preview));

} catch (Exception ex) {
logger.log(Level.SEVERE, "Error getting dataset external tool URL: " + ex.getMessage(), ex);
return error(Response.Status.INTERNAL_SERVER_ERROR,
"An error occurred while generating the external tool URL.");
}
}

// This method provides a callback for an external tool to retrieve it's
// parameters/api URLs. If the request is authenticated, e.g. by it being
// signed, the api URLs will be signed. If a guest request is made, the URLs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ExternalToolsApi extends AbstractApiBean {

@GET
public Response getExternalTools() {
//ToDo - allow filtering by scope, tool type, file content type, etc?
return externalTools.getExternalTools();
}

Expand Down
Loading