From cfd9da8af1c7ad522381eff38c2750e668eb24b8 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:55:39 -0400 Subject: [PATCH] added api for optimized lookup for a user --- .../iq/dataverse/PermissionServiceBean.java | 49 ++++++++++++ .../edu/harvard/iq/dataverse/api/Users.java | 24 +++++- .../GetUserPermittedCollectionsCommand.java | 60 ++++++++++++++ .../edu/harvard/iq/dataverse/api/UsersIT.java | 78 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 9 +++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index a389cbc735b..cf4aae57174 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -100,6 +100,45 @@ public class PermissionServiceBean { @Inject DatasetVersionFilesServiceBean datasetVersionFilesServiceBean; + private static final String LIST_ALL_DATAVERSES_USER_HAS_PERMISSION = "WITH grouplist AS (\n" + + " SELECT explicitgroup_authenticateduser.explicitgroup_id as id FROM explicitgroup_authenticateduser\n" + + " WHERE explicitgroup_authenticateduser.containedauthenticatedusers_id = @USERID\n" + + ")\n" + + "\n" + + "SELECT * FROM DATAVERSE WHERE id IN (\n" + + " SELECT definitionpoint_id \n" + + " FROM roleassignment\n" + + " WHERE roleassignment.assigneeidentifier IN (\n" + + " SELECT CONCAT('&explicit/', explicitgroup.groupalias) as assignee\n" + + " FROM explicitgroup\n" + + " WHERE explicitgroup.id IN (\n" + + " (\n" + + " SELECT explicitgroup.id id\n" + + " FROM explicitgroup \n" + + " WHERE explicitgroup.id IN (SELECT id FROM grouplist)\n" + + " ) UNION (\n" + + " SELECT explicitgroup_explicitgroup.containedexplicitgroups_id id\n" + + " FROM explicitgroup_explicitgroup\n" + + " WHERE explicitgroup_explicitgroup.explicitgroup_id IN (SELECT id FROM grouplist)\n" + + " AND \n" + + " (SELECT count(*)\n" + + " FROM dataverserole\n" + + " WHERE dataverserole.id = roleassignment.role_id and (dataverserole.permissionbits & @PERMISSIONBIT !=0)) > 0\n" + + " )\n" + + " )\n" + + " ) UNION (\n" + + " SELECT definitionpoint_id \n" + + " FROM roleassignment\n" + + " WHERE roleassignment.assigneeidentifier = (\n" + + " SELECT CONCAT('@', authenticateduser.useridentifier)\n" + + " FROM authenticateduser \n" + + " WHERE authenticateduser.id = @USERID)\n" + + " AND \n" + + " (SELECT count(*)\n" + + " FROM dataverserole\n" + + " WHERE dataverserole.id = roleassignment.role_id and (dataverserole.permissionbits & @PERMISSIONBIT !=0)) > 0\n" + + " )\n" + + ")"; /** * A request-level permission query (e.g includes IP ras). */ @@ -888,4 +927,14 @@ private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion Long result = em.createQuery(criteriaQuery).getSingleResult(); return result > 0; } + + public List findPermittedCollections(AuthenticatedUser user, int permissionBit) { + if (user != null) { + String sqlCode = LIST_ALL_DATAVERSES_USER_HAS_PERMISSION + .replace("@USERID", String.valueOf(user.getId())) + .replace("@PERMISSIONBIT", String.valueOf(permissionBit)); + return em.createNativeQuery(sqlCode, Dataverse.class).getResultList(); + } + return null; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index c1a7c95dbff..6a0bf8857a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -10,6 +10,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.impl.ChangeUserIdentifierCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetUserPermittedCollectionsCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetUserTracesCommand; import edu.harvard.iq.dataverse.engine.command.impl.MergeInAccountCommand; import edu.harvard.iq.dataverse.engine.command.impl.RevokeAllRolesCommand; @@ -260,5 +261,26 @@ public Response getTracesElement(@Context ContainerRequestContext crc, @Context return ex.getResponse(); } } - + @GET + @AuthRequired + @Path("{identifier}/allowedcollections/{permission}") + @Produces("text/csv, application/json") + public Response getUserPermittedCollections(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("permission") String permission) { + AuthenticatedUser authenticatedUser = null; + try { + authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + if (!authenticatedUser.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "This API call can be used by superusers only"); + } + } catch (WrappedResponse ex) { + return error(Response.Status.UNAUTHORIZED, "Authentication is required."); + } + try { + AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier); + JsonObjectBuilder jsonObj = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission)); + return ok(jsonObj); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java new file mode 100644 index 00000000000..8974baf1ced --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java @@ -0,0 +1,60 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; + +import java.util.List; +import java.util.logging.Logger; + +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; + +@RequiredPermissions({}) +public class GetUserPermittedCollectionsCommand extends AbstractCommand { + private static final Logger logger = Logger.getLogger(GetUserPermittedCollectionsCommand.class.getCanonicalName()); + + private DataverseRequest request; + private AuthenticatedUser user; + private String permission; + public GetUserPermittedCollectionsCommand(DataverseRequest request, AuthenticatedUser user, String permission) { + super(request, (DvObject) null); + this.request = request; + this.user = user; + this.permission = permission; + } + + @Override + public JsonObjectBuilder execute(CommandContext ctxt) throws CommandException { + if (user == null) { + throw new CommandException("User not found.", this); + } + int permissionBit; + try { + permissionBit = permission.equalsIgnoreCase("any") ? + Integer.MAX_VALUE : (1 << Permission.valueOf(permission).ordinal()); + } catch (IllegalArgumentException e) { + throw new CommandException("Permission not valid.", this); + } + List collections = ctxt.permissions().findPermittedCollections(user, permissionBit); + if (collections != null) { + JsonObjectBuilder job = Json.createObjectBuilder(); + JsonArrayBuilder jab = Json.createArrayBuilder(); + for (Dataverse dv : collections) { + jab.add(json(dv)); + } + job.add("count", collections.size()); + job.add("items", jab); + return job; + } + return null; + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java index 1003c1a990c..438dc74461b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.util.BundleUtil; import io.restassured.RestAssured; import static io.restassured.RestAssured.given; import io.restassured.http.ContentType; @@ -516,6 +518,82 @@ public void testDeleteAuthenticatedUser() { } + @Test + public void testUserPermittedDataverses() { + Response createSuperuser = UtilIT.createRandomUser(); + String superuserUsername = UtilIT.getUsernameFromResponse(createSuperuser); + String superuserApiToken = UtilIT.getApiTokenFromResponse(createSuperuser); + Response toggleSuperuser = UtilIT.makeSuperUser(superuserUsername); + toggleSuperuser.then().assertThat() + .statusCode(OK.getStatusCode()); + + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + assertEquals(200, createUser.getStatusCode()); + String usernameOfUser = UtilIT.getUsernameFromResponse(createUser); + String userApiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverse1 = UtilIT.createRandomDataverse(superuserApiToken); + createDataverse1.prettyPrint(); + createDataverse1.then().assertThat() + .statusCode(CREATED.getStatusCode()); + String dataverseAlias1 = UtilIT.getAliasFromResponse(createDataverse1); + + // create a second Dataverse and add a Group with permissions + Response createDataverse2 = UtilIT.createRandomDataverse(superuserApiToken); + createDataverse2.prettyPrint(); + createDataverse2.then().assertThat() + .statusCode(CREATED.getStatusCode()); + String dataverseAlias2 = UtilIT.getAliasFromResponse(createDataverse2); + String aliasInOwner = "groupFor" + dataverseAlias2; + String displayName = "Group for " + dataverseAlias2; + Response createGroup = UtilIT.createGroup(dataverseAlias2, aliasInOwner, displayName, superuserApiToken); + String groupIdentifier = JsonPath.from(createGroup.asString()).getString("data.identifier"); + Response grantRoleResponse = UtilIT.grantRoleOnDataverse(dataverseAlias2, DataverseRole.EDITOR.toString(), groupIdentifier, superuserApiToken); + grantRoleResponse.prettyPrint(); + grantRoleResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + Response collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, userApiToken, "ViewUnpublishedDataset"); + assertEquals(403, collectionsResp.getStatusCode()); + collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, "", "ViewUnpublishedDataset"); + assertEquals(401, collectionsResp.getStatusCode()); + collectionsResp = UtilIT.getUserPermittedCollections("fakeUser", superuserApiToken, "ViewUnpublishedDataset"); + assertEquals(500, collectionsResp.getStatusCode()); + collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, superuserApiToken, "bad"); + assertEquals(500, collectionsResp.getStatusCode()); + + // Testing adding an explicit permission/role to one dataverse + collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, superuserApiToken, "DownloadFile"); + collectionsResp.prettyPrint(); + collectionsResp.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.count", equalTo(0)); + + Response assignRole = UtilIT.grantRoleOnDataverse(dataverseAlias1, DataverseRole.EDITOR.toString(), + "@" + usernameOfUser, superuserApiToken); + assignRole.prettyPrint(); + assertEquals(200, assignRole.getStatusCode()); + + collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, superuserApiToken, "DownloadFile"); + collectionsResp.prettyPrint(); + collectionsResp.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.count", equalTo(1)); + + // Add user to group and test with both explicit and group permissions + Response addToGroup = UtilIT.addToGroup(dataverseAlias2, aliasInOwner, List.of("@" + usernameOfUser), superuserApiToken); + addToGroup.prettyPrint(); + addToGroup.then().assertThat() + .statusCode(OK.getStatusCode()); + + collectionsResp = UtilIT.getUserPermittedCollections(usernameOfUser, superuserApiToken, "DownloadFile"); + collectionsResp.prettyPrint(); + collectionsResp.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.count", equalTo(2)); + } + private Response convertUserFromBcryptToSha1(long idOfBcryptUserToConvert, String password) { JsonObjectBuilder data = Json.createObjectBuilder(); data.add("builtinUserId", idOfBcryptUserToConvert); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 70f49d81b35..302bd751d45 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1344,6 +1344,15 @@ public static Response getUserTraces(String username, String apiToken) { return response; } + public static Response getUserPermittedCollections(String username, String apiToken, String permission) { + RequestSpecification requestSpecification = given(); + if (!StringUtil.isEmpty(apiToken)) { + requestSpecification.header(API_TOKEN_HTTP_HEADER, apiToken); + } + Response response = requestSpecification.get("/api/users/" + username + "/allowedcollections/" + permission); + return response; + } + public static Response reingestFile(Long fileId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken)