From 4fabfec09b0c150a7a71bb52ff53c3e1a7e858b0 Mon Sep 17 00:00:00 2001 From: Ludovic DANIEL Date: Wed, 6 Mar 2024 18:29:27 +0100 Subject: [PATCH] #9317 - Adding Integration Tests + Superusers only security check on SavedSearch API --- .../iq/dataverse/api/SavedSearches.java | 90 +++++- .../iq/dataverse/api/SavedSearchIT.java | 260 ++++++++++++++++++ 2 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/SavedSearchIT.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java b/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java index cc1d7483c29..79558fa2031 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; @@ -24,6 +25,8 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; @@ -46,8 +49,20 @@ public Response meta() { } @GET + @AuthRequired @Path("list") - public Response list() { + public Response list(@Context ContainerRequestContext crc) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } + JsonArrayBuilder savedSearchesBuilder = Json.createArrayBuilder(); List savedSearches = savedSearchSvc.findAll(); for (SavedSearch savedSearch : savedSearches) { @@ -55,13 +70,25 @@ public Response list() { savedSearchesBuilder.add(thisSavedSearch); } JsonObjectBuilder response = Json.createObjectBuilder(); - response.add("saved searches", savedSearchesBuilder); + response.add("savedSearches", savedSearchesBuilder); return ok(response); } @GET + @AuthRequired @Path("{id}") - public Response show(@PathParam("id") long id) { + public Response show(@Context ContainerRequestContext crc, @PathParam("id") long id) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } + SavedSearch savedSearch = savedSearchSvc.find(id); if (savedSearch != null) { JsonObjectBuilder response = toJson(savedSearch); @@ -89,7 +116,18 @@ private JsonObjectBuilder toJson(SavedSearch savedSearch) { } @POST - public Response add(JsonObject body) { + @AuthRequired + public Response add(@Context ContainerRequestContext crc, JsonObject body) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } if (body == null) { return error(BAD_REQUEST, "JSON is expected."); @@ -159,7 +197,7 @@ public Response add(JsonObject body) { try { SavedSearch persistedSavedSearch = savedSearchSvc.add(toPersist); - return ok("Added: " + persistedSavedSearch); + return ok("Added: " + persistedSavedSearch, Json.createObjectBuilder().add("id", persistedSavedSearch.getId())); } catch (EJBException ex) { StringBuilder errors = new StringBuilder(); Throwable throwable = ex.getCause(); @@ -172,8 +210,20 @@ public Response add(JsonObject body) { } @DELETE + @AuthRequired @Path("{id}") - public Response delete(@PathParam("id") long doomedId, @QueryParam("unlink") boolean unlink) { + public Response delete(@Context ContainerRequestContext crc, @PathParam("id") long doomedId, @QueryParam("unlink") boolean unlink) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } + SavedSearch doomed = savedSearchSvc.find(doomedId); if (doomed == null) { return error(NOT_FOUND, "Could not find saved search id " + doomedId); @@ -193,8 +243,20 @@ public Response delete(@PathParam("id") long doomedId, @QueryParam("unlink") boo } @PUT + @AuthRequired @Path("makelinks/all") - public Response makeLinksForAllSavedSearches(@QueryParam("debug") boolean debug) { + public Response makeLinksForAllSavedSearches(@Context ContainerRequestContext crc, @QueryParam("debug") boolean debug) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } + JsonObjectBuilder makeLinksResponse; try { makeLinksResponse = savedSearchSvc.makeLinksForAllSavedSearches(debug); @@ -207,8 +269,20 @@ public Response makeLinksForAllSavedSearches(@QueryParam("debug") boolean debug) } @PUT + @AuthRequired @Path("makelinks/{id}") - public Response makeLinksForSingleSavedSearch(@PathParam("id") long savedSearchIdToLookUp, @QueryParam("debug") boolean debug) { + public Response makeLinksForSingleSavedSearch(@Context ContainerRequestContext crc, @PathParam("id") long savedSearchIdToLookUp, @QueryParam("debug") boolean debug) { + + AuthenticatedUser superuser = null; + try { + superuser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (superuser == null || !superuser.isSuperuser()) { + return error(Response.Status.UNAUTHORIZED, "Superusers only."); + } + SavedSearch savedSearchToMakeLinksFor = savedSearchSvc.find(savedSearchIdToLookUp); if (savedSearchToMakeLinksFor == null) { return error(BAD_REQUEST, "Count not find saved search id " + savedSearchIdToLookUp); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SavedSearchIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SavedSearchIT.java new file mode 100644 index 00000000000..b672120c16d --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/SavedSearchIT.java @@ -0,0 +1,260 @@ +package edu.harvard.iq.dataverse.api; + +import io.restassured.RestAssured; +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; +import io.smallrye.common.constraint.Assert; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.logging.Logger; + +import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static jakarta.ws.rs.core.Response.Status.OK; +import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; + +public class SavedSearchIT { + + private static final Logger logger = Logger.getLogger(SavedSearchIT.class.getCanonicalName()); + + private String adminApiToken; + + + @BeforeAll + public static void setUpClass() { + + } + + @AfterAll + public static void afterClass() { + + } + + @Test + public void testSavedSearches() { + + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createAdminUser = UtilIT.createRandomUser(); + String adminUsername = UtilIT.getUsernameFromResponse(createAdminUser); + adminApiToken = UtilIT.getApiTokenFromResponse(createAdminUser); + UtilIT.makeSuperUser(adminUsername); + + Response createDataverseResponse = UtilIT.createRandomDataverse(adminApiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverseResponse); + + + //dataset-finch1-nolicense.p + Response createDatasetResponse1 = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, adminApiToken); + createDatasetResponse1.prettyPrint(); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse1); + + Response createDatasetResponse2 = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, adminApiToken); + createDatasetResponse2.prettyPrint(); + Integer datasetId2 = UtilIT.getDatasetIdFromResponse(createDatasetResponse2); + + // create a saved search as non superuser : UNAUTHORIZED + Response resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken) + .body(createSavedSearchJson("*", 1, dataverseId, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + // missing body + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(INTERNAL_SERVER_ERROR.getStatusCode()); + + // creatorId null + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", null, dataverseId, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + + // creatorId string + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", "1", dataverseId.toString(), "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + + // creatorId not found + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", 9999, dataverseId, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + // definitionPointId null + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", 1, null, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + + // definitionPointId string + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", "1", "9999", "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + + // definitionPointId not found + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", 1, 9999, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + // missing filter + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", 1, dataverseId)) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(OK.getStatusCode()); + + // create a saved search as superuser : OK + resp = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .body(createSavedSearchJson("*", 1, dataverseId, "subject_ss:Medicine, Health and Life Sciences")) + .contentType("application/json") + .post("/api/admin/savedsearches"); + resp.prettyPrint(); + resp.then().assertThat() + .statusCode(OK.getStatusCode()); + + JsonPath path = JsonPath.from(resp.body().asString()); + Integer createdSavedSearchId = path.getInt("data.id"); + + // get list as non superuser : UNAUTHORIZED + Response getListReponse = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/admin/savedsearches/list"); + getListReponse.prettyPrint(); + getListReponse.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + // get list as superuser : OK + getListReponse = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .get("/api/admin/savedsearches/list"); + getListReponse.prettyPrint(); + getListReponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + JsonPath path2 = JsonPath.from(getListReponse.body().asString()); + List listBeforeDelete = path2.getList("data.savedSearches"); + + // makelinks/all as non superuser : UNAUTHORIZED + Response makelinksAll = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken) + .put("/api/admin/savedsearches/makelinks/all"); + makelinksAll.prettyPrint(); + makelinksAll.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + // makelinks/all as superuser : OK + makelinksAll = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .put("/api/admin/savedsearches/makelinks/all"); + makelinksAll.prettyPrint(); + makelinksAll.then().assertThat() + .statusCode(OK.getStatusCode()); + + //delete a saved search as non superuser : UNAUTHORIZED + Response deleteReponse = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/admin/savedsearches/" + createdSavedSearchId); + deleteReponse.prettyPrint(); + deleteReponse.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + //delete a saved search as superuser : OK + deleteReponse = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .delete("/api/admin/savedsearches/" + createdSavedSearchId); + deleteReponse.prettyPrint(); + deleteReponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + // check list count minus 1 + getListReponse = RestAssured.given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminApiToken) + .get("/api/admin/savedsearches/list"); + getListReponse.prettyPrint(); + JsonPath path3 = JsonPath.from(getListReponse.body().asString()); + List listAfterDelete = path3.getList("data.savedSearches"); + Assert.assertTrue(listBeforeDelete.size() - 1 == listAfterDelete.size()); + } + + public String createSavedSearchJson(String query, Integer creatorId, Integer definitionPointId, String... filterQueries) { + + JsonArrayBuilder arr = Json.createArrayBuilder(); + for (String filterQuery : filterQueries) { + arr.add(filterQuery); + } + + JsonObjectBuilder json = Json.createObjectBuilder(); + if(query != null) json.add("query", query); + if(creatorId != null) json.add("creatorId", creatorId); + if(definitionPointId != null) json.add("definitionPointId", definitionPointId); + if(filterQueries.length > 0) json.add("filterQueries", arr); + return json.build().toString(); + } + + public String createSavedSearchJson(String query, String creatorId, String definitionPointId, String... filterQueries) { + + JsonArrayBuilder arr = Json.createArrayBuilder(); + for (String filterQuery : filterQueries) { + arr.add(filterQuery); + } + + JsonObjectBuilder json = Json.createObjectBuilder(); + if(query != null) json.add("query", query); + if(creatorId != null) json.add("creatorId", creatorId); + if(definitionPointId != null) json.add("definitionPointId", definitionPointId); + if(filterQueries.length > 0) json.add("filterQueries", arr); + return json.build().toString(); + } +}