From af976f54865708ca5102cd2c73ab8d141b777fef Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 16 Nov 2022 17:11:01 -0500 Subject: [PATCH 01/17] some straightforward fixes/cleanup for the create harvest client api (#8290) --- .../harvard/iq/dataverse/api/HarvestingClients.java | 10 ++++++---- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index d17e76c499a..d7b35a0357a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -32,7 +32,8 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -@Stateless +// huh, why was this api @Stateless?? +//@Stateless @Path("harvest/clients") public class HarvestingClients extends AbstractApiBean { @@ -162,10 +163,10 @@ public Response createHarvestingClient(String jsonBody, @PathParam("nickName") S ownerDataverse.setHarvestingClientConfigs(new ArrayList<>()); } ownerDataverse.getHarvestingClientConfigs().add(harvestingClient); - + DataverseRequest req = createDataverseRequest(findUserOrDie()); - HarvestingClient managedHarvestingClient = execCommand( new CreateHarvestingClientCommand(req, harvestingClient)); - return created( "/harvest/clients/" + nickName, harvestingConfigAsJson(managedHarvestingClient)); + harvestingClient = execCommand(new CreateHarvestingClientCommand(req, harvestingClient)); + return created( "/harvest/clients/" + nickName, harvestingConfigAsJson(harvestingClient)); } catch (JsonParseException ex) { return error( Response.Status.BAD_REQUEST, "Error parsing harvesting client: " + ex.getMessage() ); @@ -296,6 +297,7 @@ public static JsonObjectBuilder harvestingConfigAsJson(HarvestingClient harvesti return jsonObjectBuilder().add("nickName", harvestingConfig.getName()). add("dataverseAlias", harvestingConfig.getDataverse().getAlias()). add("type", harvestingConfig.getHarvestType()). + add("style", harvestingConfig.getHarvestStyle()). add("harvestUrl", harvestingConfig.getHarvestingUrl()). add("archiveUrl", harvestingConfig.getArchiveUrl()). add("archiveDescription",harvestingConfig.getArchiveDescription()). diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 4ecdc73ae6e..54b16596ab4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -903,6 +903,7 @@ public String parseHarvestingClient(JsonObject obj, HarvestingClient harvestingC harvestingClient.setName(obj.getString("nickName",null)); harvestingClient.setHarvestType(obj.getString("type",null)); + harvestingClient.setHarvestStyle(obj.getString("style", "default")); harvestingClient.setHarvestingUrl(obj.getString("harvestUrl",null)); harvestingClient.setArchiveUrl(obj.getString("archiveUrl",null)); harvestingClient.setArchiveDescription(obj.getString("archiveDescription")); From 33a5cee41720dd3c84fd8a006d8b82f3f08d0b49 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 17 Nov 2022 11:01:45 -0500 Subject: [PATCH 02/17] doc entries for the previously undocumented harvesting clients api (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 99 +++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 6d68d648cb3..54bbe67ce1e 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3200,6 +3200,105 @@ The fully expanded example above (without the environment variables) looks like Only users with superuser permissions may delete harvesting sets. +Managing Harvesting Clients +--------------------------- + +The following API can be used to create and manage "Harvesting Clients". A Harvesting Client is a configuration entry that allows your Dataverse installation to harvest and index metadata from a specific remote location, either regularly, on a configured schedule, or on a one-off basis. For more information, see the :doc:`/admin/harvestclients` section of the Admin Guide. + +List All Congigured Harvesting Clients +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Shows all the Harvesting Clients configured:: + + GET http://$SERVER/api/harvest/clients/ + +Show a specific Harvesting Client +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Shows a Harvesting Client with a defined nickname:: + + GET http://$SERVER/api/harvest/clients/$nickname + +.. code-block:: bash + + curl "http://localhost:8080/api/harvest/clients/myclient" + + { + "status":"OK", + { + "data": { + "lastDatasetsFailed": "22", + "lastDatasetsDeleted": "0", + "metadataFormat": "oai_dc", + "archiveDescription": "This Dataset is harvested from our partners. Clicking the link will take you directly to the archival source of the data.", + "archiveUrl": "https://dataverse.foo.edu", + "harvestUrl": "https://dataverse.foo.edu/oai", + "style": "dataverse", + "type": "oai", + "dataverseAlias": "fooData", + "nickName": "myClient", + "set": "fooSet", + "schedule": "none", + "status": "inActive", + "lastHarvest": "Thu Oct 13 14:48:57 EDT 2022", + "lastResult": "SUCCESS", + "lastSuccessful": "Thu Oct 13 14:48:57 EDT 2022", + "lastNonEmpty": "Thu Oct 13 14:48:57 EDT 2022", + "lastDatasetsHarvested": "137" + } + } + + +Create a Harvesting Client +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To create a new harvesting client you must supply a JSON file that describes the configuration, similarly to the output of the GET API above. The following fields are mandatory: + +- nickName: Alpha-numeric may also contain -, _, or %, but no spaces. Must also be unique in the installation. Must match the nickName in the Path +- dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. +- dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. +- harvestUrl: The URL of the remote OAI archive +- archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://dataverse.harvard.edu/ vs. https://dataverse.harvard.edu/oai. +- metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi". + +The following optional fields are supported: + +- archiveDescription: What the name suggests. If not supplied, will default to "This Dataset is harvested from our partners. Clicking the link will take you directly to the archival source of the data." +- set: The OAI set on the remote server. If not supplied, will default to none, i.e., "harvest everything". +- schedule: Harvesting schedule. Defaults to "none". +- style: Defaults to "default" - a generic OAI archive. (Make sure to use "dataverse" when configuring harvesting from another Dataverse installation). + +An example JSON file would look like this:: + + { + "nickName": "zenodo", + "dataverseAlias": "zenodoHarvested", + "type": "oai", + "harvestUrl": "https://zenodo.org/oai2d", + "archiveUrl": "https://zenodo.org", + "archiveDescription": "Moissonné depuis la collection LMOPS de l'entrepôt Zenodo. En cliquant sur ce jeu de données, vous serez redirigé vers Zenodo.", + "metadataFormat": "oai_dc", + "set": "user-lmops" + } + +.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of export below. + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=http://localhost:8080 + + curl -H X-Dataverse-key:$API_TOKEN -X POST "$SERVER_URL/api/harvest/clients/zenodo" --upload-file client.json + +The fully expanded example above (without the environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "http://localhost:8080/api/harvest/clients/zenodo" --upload-file "client.json" + +Only users with superuser permissions may create or configure harvesting clients. + + PIDs ---- From eded574cf930552b55b578404c6ba03e969d9f00 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 17 Nov 2022 12:00:20 -0500 Subject: [PATCH 03/17] more doc. corrections #8290 --- doc/sphinx-guides/source/api/native-api.rst | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 54bbe67ce1e..e33b502f43c 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3258,7 +3258,7 @@ To create a new harvesting client you must supply a JSON file that describes the - dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. - dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. - harvestUrl: The URL of the remote OAI archive -- archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://dataverse.harvard.edu/ vs. https://dataverse.harvard.edu/oai. +- archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai. - metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi". The following optional fields are supported: @@ -3296,8 +3296,34 @@ The fully expanded example above (without the environment variables) looks like curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "http://localhost:8080/api/harvest/clients/zenodo" --upload-file "client.json" + { + "status": "OK", + "data": { + "metadataFormat": "oai_dc", + "archiveDescription": "Moissonné depuis la collection LMOPS de l'entrepôt Zenodo. En cliquant sur ce jeu de données, vous serez redirigé vers Zenodo.", + "archiveUrl": "https://zenodo.org", + "harvestUrl": "https://zenodo.org/oai2d", + "style": "default", + "type": "oai", + "dataverseAlias": "zenodoHarvested", + "nickName": "zenodo", + "set": "user-lmops", + "schedule": "none", + "status": "inActive", + "lastHarvest": "N/A", + "lastSuccessful": "N/A", + "lastNonEmpty": "N/A", + "lastDatasetsHarvested": "N/A", + "lastDatasetsDeleted": "N/A" + } + } + Only users with superuser permissions may create or configure harvesting clients. +Create a Harvesting Client +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to the API above, using the same JSON format, but run on an existing client and using the PUT method instead of POST. PIDs ---- From b2e3a40753c3b47ecbc2d851a15269a927a551b7 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 17 Nov 2022 12:04:44 -0500 Subject: [PATCH 04/17] correction (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index e33b502f43c..e06ab5111e3 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3320,7 +3320,7 @@ The fully expanded example above (without the environment variables) looks like Only users with superuser permissions may create or configure harvesting clients. -Create a Harvesting Client +Modify a Harvesting Client ~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to the API above, using the same JSON format, but run on an existing client and using the PUT method instead of POST. From c704b002c810139ca87af697a314e2f155944f7a Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 17 Nov 2022 18:40:45 -0500 Subject: [PATCH 05/17] more fixes (DELETE api, etc.) #8290 --- doc/sphinx-guides/source/api/native-api.rst | 1 - .../iq/dataverse/api/HarvestingClients.java | 141 ++++++++++++++++-- .../iq/dataverse/util/json/JsonParser.java | 3 +- 3 files changed, 130 insertions(+), 15 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index e06ab5111e3..e7715725454 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3256,7 +3256,6 @@ To create a new harvesting client you must supply a JSON file that describes the - nickName: Alpha-numeric may also contain -, _, or %, but no spaces. Must also be unique in the installation. Must match the nickName in the Path - dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. -- dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. - harvestUrl: The URL of the remote OAI archive - archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai. - metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi". diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index d7b35a0357a..5fb47e93f11 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -5,12 +5,14 @@ import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.CreateHarvestingClientCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetHarvestingClientCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateHarvestingClientCommand; import edu.harvard.iq.dataverse.harvest.client.HarvesterServiceBean; import edu.harvard.iq.dataverse.harvest.client.HarvestingClientServiceBean; +import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; import javax.json.JsonObjectBuilder; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; @@ -24,6 +26,7 @@ import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -38,8 +41,8 @@ public class HarvestingClients extends AbstractApiBean { - @EJB - DataverseServiceBean dataverseService; + //@EJB + //DataverseServiceBean dataverseService; @EJB HarvesterServiceBean harvesterService; @EJB @@ -112,6 +115,10 @@ public Response harvestingClient(@PathParam("nickName") String nickName, @QueryP return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); } + // See the comment in the harvestingClients() (plural) above for the explanation + // of why we are looking up the client twice (tl;dr: to utilize the + // authorization logic in the command) + HarvestingClient retrievedHarvestingClient = null; try { @@ -144,20 +151,56 @@ public Response harvestingClient(@PathParam("nickName") String nickName, @QueryP @POST @Path("{nickName}") public Response createHarvestingClient(String jsonBody, @PathParam("nickName") String nickName, @QueryParam("key") String apiKey) throws IOException, JsonParseException { - + // Note that we don't check the user's authorization within the API + // method. Insetead, we will end up reporting a "not authorized" + // exception thrown by the Command, if this user has no permission + // to perform the action. + try ( StringReader rdr = new StringReader(jsonBody) ) { JsonObject json = Json.createReader(rdr).readObject(); + // Check that the client with this name doesn't exist yet: + // (we could simply let the command fail, but that does not result + // in a pretty report to the end user) + + HarvestingClient lookedUpClient = null; + try { + lookedUpClient = harvestingClientService.findByNickname(nickName); + } catch (Exception ex) { + logger.warning("Exception caught looking up harvesting client " + nickName + ": " + ex.getMessage()); + // let's hope that this was a fluke of some kind; we'll proceed + // with the attempt to create a new client and report an error + // if that fails too. + } + + if (lookedUpClient != null) { + return error(Response.Status.BAD_REQUEST, "Harvesting client " + nickName + " already exists"); + } + HarvestingClient harvestingClient = new HarvestingClient(); - // TODO: check that it doesn't exist yet... - harvestingClient.setName(nickName); + String dataverseAlias = jsonParser().parseHarvestingClient(json, harvestingClient); - Dataverse ownerDataverse = dataverseService.findByAlias(dataverseAlias); + if (dataverseAlias == null) { + return error(Response.Status.BAD_REQUEST, "dataverseAlias must be supplied"); + } + + // Check if the dataverseAlias supplied is valid, i.e. corresponds + // to an existing dataverse (collection): + Dataverse ownerDataverse = dataverseSvc.findByAlias(dataverseAlias); if (ownerDataverse == null) { return error(Response.Status.BAD_REQUEST, "No such dataverse: " + dataverseAlias); } + // The nickname supplied as part of the Rest path takes precedence: + harvestingClient.setName(nickName); + + if (StringUtil.isEmpty(harvestingClient.getArchiveUrl()) + || StringUtil.isEmpty(harvestingClient.getHarvestingUrl()) + || StringUtil.isEmpty(harvestingClient.getMetadataPrefix())) { + return error(Response.Status.BAD_REQUEST, "Required fields harvestUrl, archiveUrl and metadataFormat must be supplied"); + } + harvestingClient.setDataverse(ownerDataverse); if (ownerDataverse.getHarvestingClientConfigs() == null) { ownerDataverse.setHarvestingClientConfigs(new ArrayList<>()); @@ -199,15 +242,39 @@ public Response modifyHarvestingClient(String jsonBody, @PathParam("nickName") S DataverseRequest req = createDataverseRequest(findUserOrDie()); JsonObject json = Json.createReader(rdr).readObject(); - String newDataverseAlias = jsonParser().parseHarvestingClient(json, harvestingClient); + HarvestingClient newHarvestingClient = new HarvestingClient(); + String newDataverseAlias = jsonParser().parseHarvestingClient(json, newHarvestingClient); if (newDataverseAlias != null && !newDataverseAlias.equals("") && !newDataverseAlias.equals(ownerDataverseAlias)) { return error(Response.Status.BAD_REQUEST, "Bad \"dataverseAlias\" supplied. Harvesting client "+nickName+" belongs to the dataverse "+ownerDataverseAlias); } - HarvestingClient managedHarvestingClient = execCommand( new UpdateHarvestingClientCommand(req, harvestingClient)); - return created( "/datasets/" + nickName, harvestingConfigAsJson(managedHarvestingClient)); + + // Go through the supported editable fields and update the client accordingly: + + if (newHarvestingClient.getHarvestingUrl() != null) { + harvestingClient.setHarvestingUrl(newHarvestingClient.getHarvestingUrl()); + } + if (newHarvestingClient.getHarvestingSet() != null) { + harvestingClient.setHarvestingSet(newHarvestingClient.getHarvestingSet()); + } + if (newHarvestingClient.getMetadataPrefix() != null) { + harvestingClient.setMetadataPrefix(newHarvestingClient.getMetadataPrefix()); + } + if (newHarvestingClient.getArchiveUrl() != null) { + harvestingClient.setArchiveUrl(newHarvestingClient.getArchiveUrl()); + } + if (newHarvestingClient.getArchiveDescription() != null) { + harvestingClient.setArchiveDescription(newHarvestingClient.getArchiveDescription()); + } + if (newHarvestingClient.getHarvestStyle() != null) { + harvestingClient.setHarvestStyle(newHarvestingClient.getHarvestStyle()); + } + // TODO: Make schedule configurable via this API too. + + harvestingClient = execCommand( new UpdateHarvestingClientCommand(req, harvestingClient)); + return ok( "/harvest/clients/" + nickName, harvestingConfigAsJson(harvestingClient)); } catch (JsonParseException ex) { return error( Response.Status.BAD_REQUEST, "Error parsing harvesting client: " + ex.getMessage() ); @@ -219,9 +286,59 @@ public Response modifyHarvestingClient(String jsonBody, @PathParam("nickName") S } - // TODO: - // add a @DELETE method - // (there is already a DeleteHarvestingClient command) + @DELETE + @Path("{nickName}") + public Response deleteHarvestingClient(@PathParam("nickName") String nickName) throws IOException { + // Deleting a client can take a while (if there's a large amnount of + // harvested content associated with it). So instead of calling the command + // directly, we will be calling an async. service bean method. + // Without the command engine taking care of authorization, we'll need + // to check if the user has the right to do this explicitly: + + try { + User u = findUserOrDie(); + if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete harvesting clients.")); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + HarvestingClient harvestingClient = null; + + try { + harvestingClient = harvestingClientService.findByNickname(nickName); + } catch (Exception ex) { + logger.warning("Exception caught looking up harvesting client " + nickName + ": " + ex.getMessage()); + return error( Response.Status.BAD_REQUEST, "Internal error: failed to look up harvesting client " + nickName); + } + + if (harvestingClient == null) { + return error(Response.Status.NOT_FOUND, "Harvesting client " + nickName + " not found."); + } + + // Check if the client is in a state where it can be safely deleted: + + if (harvestingClient.isDeleteInProgress()) { + return error( Response.Status.BAD_REQUEST, "Harvesting client " + nickName + " is already being deleted (in progress)"); + } + + if (harvestingClient.isHarvestingNow()) { + return error( Response.Status.BAD_REQUEST, "It is not safe to delete client " + nickName + " while a harvesting job is in progress"); + } + + // Finally, delete it (asynchronously): + + try { + harvestingClientService.deleteClient(harvestingClient.getId()); + } catch (Exception ex) { + return error( Response.Status.BAD_REQUEST, "Internal error: failed to delete harvesting client " + nickName); + } + + + return ok("Harvesting Client " + nickName + ": delete in progress"); + } + // Methods for managing harvesting runs (jobs): diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 54b16596ab4..3e5c096aff0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -902,11 +902,10 @@ public String parseHarvestingClient(JsonObject obj, HarvestingClient harvestingC String dataverseAlias = obj.getString("dataverseAlias",null); harvestingClient.setName(obj.getString("nickName",null)); - harvestingClient.setHarvestType(obj.getString("type",null)); harvestingClient.setHarvestStyle(obj.getString("style", "default")); harvestingClient.setHarvestingUrl(obj.getString("harvestUrl",null)); harvestingClient.setArchiveUrl(obj.getString("archiveUrl",null)); - harvestingClient.setArchiveDescription(obj.getString("archiveDescription")); + harvestingClient.setArchiveDescription(obj.getString("archiveDescription", BundleUtil.getStringFromBundle("harvestclients.viewEditDialog.archiveDescription.default.generic"))); harvestingClient.setMetadataPrefix(obj.getString("metadataFormat",null)); harvestingClient.setHarvestingSet(obj.getString("set",null)); From fda574ac022cd5554874ba640d915cebe0dd96cc Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 18 Nov 2022 10:52:16 -0500 Subject: [PATCH 06/17] More small fixes (#8290) --- .../edu/harvard/iq/dataverse/api/HarvestingClients.java | 9 ++++++--- .../iq/dataverse/harvest/client/HarvestingClient.java | 4 +++- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index 5fb47e93f11..d4c096efdbc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -12,6 +12,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateHarvestingClientCommand; import edu.harvard.iq.dataverse.harvest.client.HarvesterServiceBean; import edu.harvard.iq.dataverse.harvest.client.HarvestingClientServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; import javax.json.JsonObjectBuilder; @@ -22,7 +23,6 @@ import java.util.List; import java.util.logging.Logger; import javax.ejb.EJB; -import javax.ejb.Stateless; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; @@ -35,8 +35,6 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -// huh, why was this api @Stateless?? -//@Stateless @Path("harvest/clients") public class HarvestingClients extends AbstractApiBean { @@ -195,6 +193,11 @@ public Response createHarvestingClient(String jsonBody, @PathParam("nickName") S // The nickname supplied as part of the Rest path takes precedence: harvestingClient.setName(nickName); + // Populate the description field, if none is supplied: + if (harvestingClient.getArchiveDescription() == null) { + harvestingClient.setArchiveDescription(BundleUtil.getStringFromBundle("harvestclients.viewEditDialog.archiveDescription.default.generic")); + } + if (StringUtil.isEmpty(harvestingClient.getArchiveUrl()) || StringUtil.isEmpty(harvestingClient.getHarvestingUrl()) || StringUtil.isEmpty(harvestingClient.getMetadataPrefix())) { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java index 32365e17852..aeb010fad6d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java @@ -188,7 +188,9 @@ public String getHarvestingUrl() { } public void setHarvestingUrl(String harvestingUrl) { - this.harvestingUrl = harvestingUrl.trim(); + if (harvestingUrl != null) { + this.harvestingUrl = harvestingUrl.trim(); + } } private String archiveUrl; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 3e5c096aff0..905479c4e0d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -905,7 +905,7 @@ public String parseHarvestingClient(JsonObject obj, HarvestingClient harvestingC harvestingClient.setHarvestStyle(obj.getString("style", "default")); harvestingClient.setHarvestingUrl(obj.getString("harvestUrl",null)); harvestingClient.setArchiveUrl(obj.getString("archiveUrl",null)); - harvestingClient.setArchiveDescription(obj.getString("archiveDescription", BundleUtil.getStringFromBundle("harvestclients.viewEditDialog.archiveDescription.default.generic"))); + harvestingClient.setArchiveDescription(obj.getString("archiveDescription", null)); harvestingClient.setMetadataPrefix(obj.getString("metadataFormat",null)); harvestingClient.setHarvestingSet(obj.getString("set",null)); From ae28d0a0a17bdaf9097ed3b7b1f1923071ddb649 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 18 Nov 2022 10:55:49 -0500 Subject: [PATCH 07/17] documented delete client api too (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index e7715725454..64d08696294 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3287,13 +3287,13 @@ An example JSON file would look like this:: export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx export SERVER_URL=http://localhost:8080 - curl -H X-Dataverse-key:$API_TOKEN -X POST "$SERVER_URL/api/harvest/clients/zenodo" --upload-file client.json + curl -H X-Dataverse-key:$API_TOKEN -X POST -H "Content-Type: application/json" "$SERVER_URL/api/harvest/clients/zenodo" --upload-file client.json The fully expanded example above (without the environment variables) looks like this: .. code-block:: bash - curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "http://localhost:8080/api/harvest/clients/zenodo" --upload-file "client.json" + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST -H "Content-Type: application/json" "http://localhost:8080/api/harvest/clients/zenodo" --upload-file "client.json" { "status": "OK", @@ -3324,6 +3324,18 @@ Modify a Harvesting Client Similar to the API above, using the same JSON format, but run on an existing client and using the PUT method instead of POST. +Delete a Harvesting Client +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Self-explanatory: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "http://localhost:8080/api/harvest/clients/$nickName" + +Only users with superuser permissions may delete harvesting clients. + + PIDs ---- From 837c4912c86a5d35c57e916620f509fb8ad032f1 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 18 Nov 2022 11:19:45 -0500 Subject: [PATCH 08/17] toned down an .info logger (#8290) --- .../java/edu/harvard/iq/dataverse/api/HarvestingClients.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index d4c096efdbc..370f6ea5d98 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -39,8 +39,6 @@ public class HarvestingClients extends AbstractApiBean { - //@EJB - //DataverseServiceBean dataverseService; @EJB HarvesterServiceBean harvesterService; @EJB @@ -124,7 +122,7 @@ public Response harvestingClient(@PathParam("nickName") String nickName, @QueryP // exception, that already has a proper HTTP response in it. retrievedHarvestingClient = execCommand(new GetHarvestingClientCommand(createDataverseRequest(findUserOrDie()), harvestingClient)); - logger.info("retrieved Harvesting Client " + retrievedHarvestingClient.getName() + " with the GetHarvestingClient command."); + logger.fine("retrieved Harvesting Client " + retrievedHarvestingClient.getName() + " with the GetHarvestingClient command."); } catch (WrappedResponse wr) { return wr.getResponse(); } catch (Exception ex) { From a667876a0180b01e693e407d0f8e29f8b45f083e Mon Sep 17 00:00:00 2001 From: landreev Date: Mon, 28 Nov 2022 15:34:33 -0500 Subject: [PATCH 09/17] Update doc/sphinx-guides/source/api/native-api.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index ce91db3457b..80a622b763f 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3205,7 +3205,7 @@ Managing Harvesting Clients The following API can be used to create and manage "Harvesting Clients". A Harvesting Client is a configuration entry that allows your Dataverse installation to harvest and index metadata from a specific remote location, either regularly, on a configured schedule, or on a one-off basis. For more information, see the :doc:`/admin/harvestclients` section of the Admin Guide. -List All Congigured Harvesting Clients +List All Configured Harvesting Clients ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shows all the Harvesting Clients configured:: From 88f78f891c0f0cc90f384e02582bd63b86b53c3f Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 28 Nov 2022 15:44:02 -0500 Subject: [PATCH 10/17] cosmetic guide improvements (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 80a622b763f..c3b01b2aba0 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3255,16 +3255,15 @@ Create a Harvesting Client To create a new harvesting client you must supply a JSON file that describes the configuration, similarly to the output of the GET API above. The following fields are mandatory: - nickName: Alpha-numeric may also contain -, _, or %, but no spaces. Must also be unique in the installation. Must match the nickName in the Path -- dataverseAlias: The alias of an existing collection where harvested datasets will be deposited. +- dataverseAlias: The alias of an existing collection where harvested datasets will be deposited - harvestUrl: The URL of the remote OAI archive -- archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai. -- metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi". +- archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai +- metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi" The following optional fields are supported: - archiveDescription: What the name suggests. If not supplied, will default to "This Dataset is harvested from our partners. Clicking the link will take you directly to the archival source of the data." - set: The OAI set on the remote server. If not supplied, will default to none, i.e., "harvest everything". -- schedule: Harvesting schedule. Defaults to "none". - style: Defaults to "default" - a generic OAI archive. (Make sure to use "dataverse" when configuring harvesting from another Dataverse installation). An example JSON file would look like this:: From 79da3efadd4878e437e8da907a06345271f1a522 Mon Sep 17 00:00:00 2001 From: landreev Date: Mon, 28 Nov 2022 15:54:37 -0500 Subject: [PATCH 11/17] Update src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java Co-authored-by: Philip Durbin --- .../java/edu/harvard/iq/dataverse/api/HarvestingClients.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index 370f6ea5d98..d04e764e5ef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -148,7 +148,7 @@ public Response harvestingClient(@PathParam("nickName") String nickName, @QueryP @Path("{nickName}") public Response createHarvestingClient(String jsonBody, @PathParam("nickName") String nickName, @QueryParam("key") String apiKey) throws IOException, JsonParseException { // Note that we don't check the user's authorization within the API - // method. Insetead, we will end up reporting a "not authorized" + // method. Instead, we will end up reporting a "not authorized" // exception thrown by the Command, if this user has no permission // to perform the action. From ba07ad9a43ca0d8dff3a47799e6907572f0fb6fe Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 28 Nov 2022 16:33:01 -0500 Subject: [PATCH 12/17] more cosmetic guide fixes (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index c3b01b2aba0..8fa35830356 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3258,7 +3258,7 @@ To create a new harvesting client you must supply a JSON file that describes the - dataverseAlias: The alias of an existing collection where harvested datasets will be deposited - harvestUrl: The URL of the remote OAI archive - archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai -- metadataFormat: A supported metadata format. For example, "oai_dc" or "ddi" +- metadataFormat: A supported metadata format. As of writing this the supported formats are "oai_dc", "oai_ddi" and "dataverse_json". The following optional fields are supported: From 469e74332ae598079dadf257498490ee3e85672b Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 28 Nov 2022 18:34:38 -0500 Subject: [PATCH 13/17] added simple restassured tests (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 11 +- .../iq/dataverse/api/HarvestingClientsIT.java | 122 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 8fa35830356..003604cf531 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3252,9 +3252,14 @@ Shows a Harvesting Client with a defined nickname:: Create a Harvesting Client ~~~~~~~~~~~~~~~~~~~~~~~~~~ -To create a new harvesting client you must supply a JSON file that describes the configuration, similarly to the output of the GET API above. The following fields are mandatory: +To create a new harvesting client:: + + POST http://$SERVER/api/harvest/clients/$nickname + +``nickName`` is the name identifying the new client. It should be alpha-numeric and may also contain -, _, or %, but no spaces. Must also be unique in the installation. + +You must supply a JSON file that describes the configuration, similarly to the output of the GET API above. The following fields are mandatory: -- nickName: Alpha-numeric may also contain -, _, or %, but no spaces. Must also be unique in the installation. Must match the nickName in the Path - dataverseAlias: The alias of an existing collection where harvested datasets will be deposited - harvestUrl: The URL of the remote OAI archive - archiveUrl: The URL of the remote archive that will be used in the redirect links pointing back to the archival locations of the harvested records. It may or may not be on the same server as the harvestUrl above. If this OAI archive is another Dataverse installation, it will be the same URL as harvestUrl minus the "/oai". For example: https://demo.dataverse.org/ vs. https://demo.dataverse.org/oai @@ -3266,6 +3271,8 @@ The following optional fields are supported: - set: The OAI set on the remote server. If not supplied, will default to none, i.e., "harvest everything". - style: Defaults to "default" - a generic OAI archive. (Make sure to use "dataverse" when configuring harvesting from another Dataverse installation). +Generally, the API will accept the output of the GET version of the API for an existing client as valid input, but some fields will be ignored. For example, as of writing this there is no way to configure a harvesting schedule via this API. + An example JSON file would look like this:: { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java new file mode 100644 index 00000000000..9eac3545e54 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -0,0 +1,122 @@ +package edu.harvard.iq.dataverse.api; + +import java.util.logging.Logger; +import com.jayway.restassured.RestAssured; +import static com.jayway.restassured.RestAssured.given; +import org.junit.Test; +import com.jayway.restassured.response.Response; +import static org.hamcrest.CoreMatchers.equalTo; +import static junit.framework.Assert.assertEquals; +import org.junit.BeforeClass; + +/** + * extremely minimal (for now) API tests for creating OAI clients. + */ +public class HarvestingClientsIT { + + private static final Logger logger = Logger.getLogger(HarvestingClientsIT.class.getCanonicalName()); + + private static final String harvestClientsApi = "/api/harvest/clients/"; + private static final String harvestCollection = "root"; + private static final String harvestUrl = "https://demo.dataverse.org/oai"; + private static final String archiveUrl = "https://demo.dataverse.org"; + private static final String harvestMetadataFormat = "oai_dc"; + private static final String archiveDescription = "RestAssured harvesting client test"; + + @BeforeClass + public static void setUpClass() { + RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + } + + private void setupUsers() { + Response cu0 = UtilIT.createRandomUser(); + normalUserAPIKey = UtilIT.getApiTokenFromResponse(cu0); + Response cu1 = UtilIT.createRandomUser(); + String un1 = UtilIT.getUsernameFromResponse(cu1); + Response u1a = UtilIT.makeSuperUser(un1); + adminUserAPIKey = UtilIT.getApiTokenFromResponse(cu1); + } + + private String normalUserAPIKey; + private String adminUserAPIKey; + + @Test + public void testCreateEditDeleteClient() { + setupUsers(); + String nickName = UtilIT.getRandomString(6); + + + String clientApiPath = String.format(harvestClientsApi+"%s", nickName); + String clientJson = String.format("{\"dataverseAlias\":\"%s\"," + + "\"type\":\"oai\"," + + "\"harvestUrl\":\"%s\"," + + "\"archiveUrl\":\"%s\"," + + "\"metadataFormat\":\"%s\"}", + harvestCollection, harvestUrl, archiveUrl, harvestMetadataFormat); + + + // Try to create a client as normal user, should fail: + + Response rCreate = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, normalUserAPIKey) + .body(clientJson) + .post(clientApiPath); + assertEquals(401, rCreate.getStatusCode()); + + + // Try to create the same as admin user, should succeed: + + rCreate = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) + .body(clientJson) + .post(clientApiPath); + assertEquals(201, rCreate.getStatusCode()); + + // Try to update the client we have just created: + + String updateJson = String.format("{\"archiveDescription\":\"%s\"}", archiveDescription); + + Response rUpdate = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) + .body(updateJson) + .put(clientApiPath); + assertEquals(200, rUpdate.getStatusCode()); + + // Now let's retrieve the client we've just created and edited: + + Response getClientResponse = given() + .get(clientApiPath); + + logger.info("getClient.getStatusCode(): " + getClientResponse.getStatusCode()); + logger.info("getClient printresponse: " + getClientResponse.prettyPrint()); + assertEquals(200, getClientResponse.getStatusCode()); + + // ... and validate the values: + + getClientResponse.then().assertThat() + .body("status", equalTo(AbstractApiBean.STATUS_OK)) + .body("data.type", equalTo("oai")) + .body("data.nickName", equalTo(nickName)) + .body("data.archiveDescription", equalTo(archiveDescription)) + .body("data.dataverseAlias", equalTo(harvestCollection)) + .body("data.harvestUrl", equalTo(harvestUrl)) + .body("data.archiveUrl", equalTo(archiveUrl)) + .body("data.metadataFormat", equalTo(harvestMetadataFormat)); + + // Try to delete the client as normal user should fail: + + Response rDelete = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, normalUserAPIKey) + .delete(clientApiPath); + logger.info("rDelete.getStatusCode(): " + rDelete.getStatusCode()); + assertEquals(401, rDelete.getStatusCode()); + + // Try to delete as admin user should work: + + rDelete = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) + .delete(clientApiPath); + logger.info("rDelete.getStatusCode(): " + rDelete.getStatusCode()); + assertEquals(200, rDelete.getStatusCode()); + } +} From 83171f32fa82d2715ecc27f5c85ee14ba7e05199 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 28 Nov 2022 18:44:02 -0500 Subject: [PATCH 14/17] cosmetic (#8290) --- doc/sphinx-guides/source/api/native-api.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 003604cf531..9da98b2f02b 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3278,7 +3278,6 @@ An example JSON file would look like this:: { "nickName": "zenodo", "dataverseAlias": "zenodoHarvested", - "type": "oai", "harvestUrl": "https://zenodo.org/oai2d", "archiveUrl": "https://zenodo.org", "archiveDescription": "Moissonné depuis la collection LMOPS de l'entrepôt Zenodo. En cliquant sur ce jeu de données, vous serez redirigé vers Zenodo.", From 79883e35d89ee65f4f5e5cf510c7d282cc685c1c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 29 Nov 2022 14:15:03 -0500 Subject: [PATCH 15/17] typo (and force Jenkins run) #8290 --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 9da98b2f02b..5e72b1f0263 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3212,7 +3212,7 @@ Shows all the Harvesting Clients configured:: GET http://$SERVER/api/harvest/clients/ -Show a specific Harvesting Client +Show a Specific Harvesting Client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shows a Harvesting Client with a defined nickname:: From 9d81716b30cbfe701718fc81b1d51039562395db Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 29 Nov 2022 17:11:24 -0500 Subject: [PATCH 16/17] added the new restassured test to the list (#8290) --- tests/integration-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration-tests.txt b/tests/integration-tests.txt index 85b37c79835..85670e8324a 100644 --- a/tests/integration-tests.txt +++ b/tests/integration-tests.txt @@ -1 +1 @@ -DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT,ExternalToolsIT,AccessIT,DuplicateFilesIT,DownloadFilesIT,LinkIT,DeleteUsersIT,DeactivateUsersIT,AuxiliaryFilesIT,InvalidCharactersIT,LicensesIT,NotificationsIT,BagIT +DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,HarvestingClientsIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT,ExternalToolsIT,AccessIT,DuplicateFilesIT,DownloadFilesIT,LinkIT,DeleteUsersIT,DeactivateUsersIT,AuxiliaryFilesIT,InvalidCharactersIT,LicensesIT,NotificationsIT,BagIT From 8794c07dafddd16c91f6ac931cbd03017d429d77 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 1 Dec 2022 13:57:44 -0500 Subject: [PATCH 17/17] making the create/edit APIs superuser-only (#8290) --- .../iq/dataverse/api/HarvestingClients.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java index d04e764e5ef..42534514b68 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/HarvestingClients.java @@ -147,10 +147,16 @@ public Response harvestingClient(@PathParam("nickName") String nickName, @QueryP @POST @Path("{nickName}") public Response createHarvestingClient(String jsonBody, @PathParam("nickName") String nickName, @QueryParam("key") String apiKey) throws IOException, JsonParseException { - // Note that we don't check the user's authorization within the API - // method. Instead, we will end up reporting a "not authorized" - // exception thrown by the Command, if this user has no permission - // to perform the action. + // Per the discussion during the QA of PR #9174, we decided to make + // the create/edit APIs superuser-only (the delete API was already so) + try { + User u = findUserOrDie(); + if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can create harvesting clients.")); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } try ( StringReader rdr = new StringReader(jsonBody) ) { JsonObject json = Json.createReader(rdr).readObject(); @@ -225,6 +231,15 @@ public Response createHarvestingClient(String jsonBody, @PathParam("nickName") S @PUT @Path("{nickName}") public Response modifyHarvestingClient(String jsonBody, @PathParam("nickName") String nickName, @QueryParam("key") String apiKey) throws IOException, JsonParseException { + try { + User u = findUserOrDie(); + if ((!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { + throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can modify harvesting clients.")); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + HarvestingClient harvestingClient = null; try { harvestingClient = harvestingClientService.findByNickname(nickName); @@ -293,8 +308,7 @@ public Response deleteHarvestingClient(@PathParam("nickName") String nickName) t // Deleting a client can take a while (if there's a large amnount of // harvested content associated with it). So instead of calling the command // directly, we will be calling an async. service bean method. - // Without the command engine taking care of authorization, we'll need - // to check if the user has the right to do this explicitly: + try { User u = findUserOrDie();