diff --git a/Makefile b/Makefile index 2bda09c3b7..2f8bda0310 100644 --- a/Makefile +++ b/Makefile @@ -331,8 +331,10 @@ clean-metals: ## clean SBT and Metals related stuff @rm -rf .bsp @rm -rf .metals @rm -rf target + @sbt "clean" -clean: docs-clean clean-local-tmp clean-docker clean-sipi-tmp clean-sipi-projects ## clean build artifacts + +clean: docs-clean clean-local-tmp clean-docker clean-sipi-tmp ## clean build artifacts @rm -rf .env .PHONY: clean-sipi-tmp diff --git a/docs/03-apis/api-admin/index.md b/docs/03-apis/api-admin/index.md index 191bf5b6ab..959009f2d4 100644 --- a/docs/03-apis/api-admin/index.md +++ b/docs/03-apis/api-admin/index.md @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 --> -# Knora Admin API +# DSP Admin API -The Knora admin API makes it possible to administer Knora projects, users, user groups, permissions, and hierarchical lists. +The DSP Admin API makes it possible to administer projects, users, user groups, permissions, and hierarchical lists. - [Introduction](introduction.md) - [Overview](overview.md) diff --git a/docs/03-apis/api-admin/users.md b/docs/03-apis/api-admin/users.md index 003f787236..6a92bcc826 100644 --- a/docs/03-apis/api-admin/users.md +++ b/docs/03-apis/api-admin/users.md @@ -130,8 +130,7 @@ specified by the `id` in the request body as below: ### Delete user - Required permission: SystemAdmin / self - - Remark: The same as updating a user and changing `status` to - `false`. To un-delete, set `status` to `true`. + - Remark: The same as updating a user and changing `status` to `false`. To un-delete, set `status` to `true`. - PUT: `/admin/users/iri//Status` - BODY: @@ -144,8 +143,7 @@ specified by the `id` in the request body as below: ### Delete user (-\update user)** - Required permission: SystemAdmin / self - - Remark: The same as updating a user and changing `status` to - `false`. To un-delete, set `status` to `true`. + - Remark: The same as updating a user and changing `status` to `false`. To un-delete, set `status` to `true`. - DELETE: `/admin/users/iri/` - BODY: empty @@ -158,13 +156,14 @@ specified by the `id` in the request body as below: ### Add/remove user to/from project - - Required permission: SystemAdmin / ProjectAdmin / self (if - project self-assignment is enabled) + - Required permission: SystemAdmin / ProjectAdmin / self (if project self-assignment is enabled) - Required information: project IRI, user IRI - Effects: `knora-base:isInProject` user property - POST / DELETE: `/admin/users/iri//project-memberships/` - BODY: empty +Note: When a user is project admin in the same project, his project admin membership will be removed as well. + ## User's group membership operations ### Get user's project admin memberships @@ -179,6 +178,8 @@ specified by the `id` in the request body as below: - POST / DELETE: `/admin/users/iri//project-admin-memberships/` - BODY: empty +Note: In order to add a user to a project admin group, the user needs to be member of that project. + ### Get user's group memberships** - GET: `/admin/users/iri//group-memberships` diff --git a/docs/05-internals/design/principles/design-overview.md b/docs/05-internals/design/principles/design-overview.md index c33e8b530d..f4c7bc8dbb 100644 --- a/docs/05-internals/design/principles/design-overview.md +++ b/docs/05-internals/design/principles/design-overview.md @@ -29,7 +29,7 @@ DSP-API supports different versions of its API for working with humanities data: - [DSP-API v1](../../../03-apis/api-v1/index.md), legacy API compatibile with applications that used the prototype software. -There is also a [Knora admin API](../../../03-apis/api-admin/index.md) for +There is also an [Admin API](../../../03-apis/api-admin/index.md) for administering DSP projects. The DSP-API code base includes some functionality that is shared by these different diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala index 73b7605a9d..f8eeb1e9b9 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala @@ -837,13 +837,16 @@ case class UserChangeRequestADM( throw BadRequestException("Too many parameters sent for system admin membership change.") } - // change project memberships - if (projects.isDefined && parametersCount > 1) { + // change project memberships (could also involve changing projectAdmin memberships) + if ( + projects.isDefined && projectsAdmin.isDefined && parametersCount > 2 || + projects.isDefined && !projectsAdmin.isDefined && parametersCount > 1 + ) { throw BadRequestException("Too many parameters sent for project membership change.") } - // change projectAdmin memberships - if (projectsAdmin.isDefined && parametersCount > 1) { + // change projectAdmin memberships only (without changing project memberships) + if (projectsAdmin.isDefined && !projects.isDefined && parametersCount > 1) { throw BadRequestException("Too many parameters sent for projectAdmin membership change.") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala index 8e288a3802..02043318d9 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala @@ -694,7 +694,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the user's IRI. * @param projectIri the project's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. * @return @@ -779,7 +778,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the user's IRI. * @param projectIri the project's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. * @return @@ -826,7 +824,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde ) currentProjectMembershipIris = currentProjectMemberships.map(_.id) - // check if user is not already a member and if he is then remove the project from to list + // check if user is a member and if he is then remove the project from to list updatedProjectMembershipIris = if (currentProjectMembershipIris.contains(projectIri)) { currentProjectMembershipIris diff Seq(projectIri) @@ -836,10 +834,29 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde ) } + // get users current project admin membership list + currentProjectAdminMemberships <- userProjectAdminMembershipsGetADM( + userIri = userIri, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) + + currentProjectAdminMembershipIris: Seq[IRI] = currentProjectAdminMemberships.map(_.id) + + // in case the user has an admin membership for that project, remove it as well + maybeUpdatedProjectAdminMembershipIris = if (currentProjectAdminMembershipIris.contains(projectIri)) { + Some( + currentProjectAdminMembershipIris.filterNot(p => p == projectIri) + ) + } else None + // create the update request by using the SystemUser result <- updateUserADM( userIri = userIri, - userUpdatePayload = UserChangeRequestADM(projects = Some(updatedProjectMembershipIris)), + userUpdatePayload = UserChangeRequestADM( + projects = Some(updatedProjectMembershipIris), + projectsAdmin = maybeUpdatedProjectAdminMembershipIris + ), requestingUser = KnoraSystemInstances.Users.SystemUser, apiRequestID = apiRequestID ) @@ -859,7 +876,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * Returns the user's project admin group memberships as a sequence of [[IRI]] * * @param userIri the user's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. * @return a [[UserProjectMembershipsGetResponseV1]]. @@ -912,7 +928,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * is a member of the project admin group. * * @param userIri the user's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. * @return a [[UserProjectMembershipsGetResponseV1]]. @@ -942,10 +957,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the user's IRI. * @param projectIri the project's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. - * @return + * @return a [[UserOperationResponseADM]]. */ private def userProjectAdminMembershipAddRequestADM( userIri: IRI, @@ -983,6 +997,22 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") // get users current project membership list + currentProjectMemberships <- userProjectMembershipsGetADM( + userIri = userIri, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) + + currentProjectMembershipIris = currentProjectMemberships.map(_.id) + + // check if user is already project member and if not throw exception + + _ = if (!currentProjectMembershipIris.contains(projectIri)) { + throw BadRequestException( + s"User $userIri is not a member of project $projectIri. A user needs to be a member of the project to be added as project admin." + ) + } + + // get users current project admin membership list currentProjectAdminMemberships <- userProjectAdminMembershipsGetADM( userIri = userIri, requestingUser = KnoraSystemInstances.Users.SystemUser, @@ -991,7 +1021,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde currentProjectAdminMembershipIris: Seq[IRI] = currentProjectAdminMemberships.map(_.id) - // check if user is already member and if not then append to list + // check if user is already project admin and if not then append to list updatedProjectAdminMembershipIris = if (!currentProjectAdminMembershipIris.contains(projectIri)) { currentProjectAdminMembershipIris :+ projectIri @@ -1026,10 +1056,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the user's IRI. * @param projectIri the project's IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. - * @return + * @return a [[UserOperationResponseADM]] */ private def userProjectAdminMembershipRemoveRequestADM( userIri: IRI, @@ -1109,7 +1138,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * Returns the user's group memberships as a sequence of [[GroupADM]] * * @param userIri the IRI of the user. - * * @param requestingUser the requesting user. * @return a sequence of [[GroupADM]]. */ @@ -1162,7 +1190,6 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the user's IRI. * @param groupIri the group IRI. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. * @return a [[UserOperationResponseADM]]. @@ -1255,6 +1282,15 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } + /** + * Removes a user from a group. + * + * @param userIri the user's IRI. + * @param groupIri the group IRI. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a [[UserOperationResponseADM]]. + */ private def userGroupMembershipRemoveRequestADM( userIri: IRI, groupIri: IRI, @@ -1343,10 +1379,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the IRI of the existing user that we want to update. * @param userUpdatePayload the updated information. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. + * @return a [[UserOperationResponseADM]]. * @throws BadRequestException if necessary parameters are not supplied. * @throws UpdateNotPerformedException if the update was not performed. */ @@ -1558,10 +1593,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * * @param userIri the IRI of the existing user that we want to update. * @param password the new password. - * * @param requestingUser the requesting user. * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. + * @return a [[UserOperationResponseADM]]. * @throws BadRequestException if necessary parameters are not supplied. * @throws UpdateNotPerformedException if the update was not performed. */ @@ -1636,9 +1670,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * - http://blog.ircmaxell.com/2012/12/seven-ways-to-screw-up-bcrypt.html * * @param userCreatePayloadADM a [[UserCreatePayloadADM]] object containing information about the new user to be created. - * - * @param requestingUser a [[UserADM]] object containing information about the requesting user. - * @return a future containing the [[UserOperationResponseADM]]. + * @param requestingUser a [[UserADM]] object containing information about the requesting user. + * @param apiRequestID the unique api request ID. + * @return a [[UserOperationResponseADM]]. */ private def createNewUserADM( userCreatePayloadADM: UserCreatePayloadADM, @@ -1761,10 +1795,13 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde /** * Tries to retrieve a [[UserADM]] either from triplestore or cache if caching is enabled. * If user is not found in cache but in triplestore, then user is written to cache. + * + * @param identifier The identifier of the user (can be IRI, e-mail or username) + * @return a [[Option[UserADM]]] */ private def getUserFromCacheOrTriplestore( identifier: UserIdentifierADM - ): Future[Option[UserADM]] = tracedFuture("admin-user-get-user-from-cache-or-triplestore") { + ): Future[Option[UserADM]] = // tracedFuture("admin-user-get-user-from-cache-or-triplestore") { if (cacheServiceSettings.cacheServiceEnabled) { // caching enabled getUserFromCache(identifier).flatMap { @@ -1793,10 +1830,13 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde log.debug("getUserFromCacheOrTriplestore - caching disabled. getting from triplestore.") getUserFromTriplestore(identifier = identifier) } - } + // } /** * Tries to retrieve a [[UserADM]] from the triplestore. + * + * @param identifier The identifier of the user (can be IRI, e-mail or username) + * @return a [[Option[UserADM]]] */ private def getUserFromTriplestore( identifier: UserIdentifierADM @@ -1836,8 +1876,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * Helper method used to create a [[UserADM]] from the [[SparqlExtendedConstructResponse]] containing user data. * * @param statements result from the SPARQL query containing user data. - * - * @return a [[UserADM]] containing the user's data. + * @return a [[Option[UserADM]]] */ private def statements2UserADM( statements: (SubjectV2, Map[SmartIri, Seq[LiteralV2]]) @@ -2114,6 +2153,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde /** * Tries to retrieve a [[UserADM]] from the cache. + * + * @param identifier the user's identifier (could be IRI, e-mail or username) + * @return a [[Option[UserADM]]] */ private def getUserFromCache(identifier: UserIdentifierADM): Future[Option[UserADM]] = tracedFuture("admin-user-get-user-from-cache") { @@ -2132,7 +2174,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * Writes the user profile to cache. * * @param user a [[UserADM]]. - * @return true if writing was successful. + * @return Unit * @throws ApplicationCacheException when there is a problem with writing the user's profile to cache. */ private def writeUserADMToCache(user: UserADM): Future[Unit] = for { @@ -2142,6 +2184,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde /** * Removes the user from cache. + * + * @param maybeUser the optional user which is removed from the cache + * @return a [[Unit]] */ private def invalidateCachedUserADM(maybeUser: Option[UserADM]): Future[Unit] = if (cacheServiceSettings.cacheServiceEnabled) { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala index 039897bd59..4d9f29eb33 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala @@ -113,7 +113,7 @@ final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State]) get { requestContext => val res: ZIO[State, Nothing, HttpResponse] = { for { - _ <- ZIO.logInfo("health route start") + _ <- ZIO.logDebug("health route start") ec <- ZIO.executor.map(_.asExecutionContext) state <- ZIO.service[State] requestingUser <- @@ -123,7 +123,7 @@ final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State]) ) .orElse(ZIO.succeed(KnoraSystemInstances.Users.AnonymousUser)) result <- healthCheck(state) - _ <- ZIO.logInfo("health route finished") @@ ZIOAspect.annotated("user-id", requestingUser.id.toString()) + _ <- ZIO.logDebug("health route finished") @@ ZIOAspect.annotated("user-id", requestingUser.id.toString()) } yield result } @@ LogAspect.logSpan("health-request") @@ LogAspect.logAnnotateCorrelationId(requestContext.request) diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFServiceManager.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFServiceManager.scala index 416f9a1593..01c5a5d5b2 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFServiceManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFServiceManager.scala @@ -35,17 +35,19 @@ object IIIFServiceManager { val layer: ZLayer[IIIFService, Nothing, IIIFServiceManager] = ZLayer { for { - iiifs <- ZIO.service[IIIFService] - } yield new IIIFServiceManager { - - override def receive(message: IIIFRequest) = message match { - case req: GetFileMetadataRequest => iiifs.getFileMetadata(req) - case req: MoveTemporaryFileToPermanentStorageRequest => iiifs.moveTemporaryFileToPermanentStorage(req) - case req: DeleteTemporaryFileRequest => iiifs.deleteTemporaryFile(req) - case req: SipiGetTextFileRequest => iiifs.getTextFileRequest(req) - case IIIFServiceGetStatus => iiifs.getStatus() - case other => ZIO.logError(s"IIIFServiceManager received an unexpected message: $other") - } - } + iiifService <- ZIO.service[IIIFService] + } yield IIIFServiceManagerImpl(iiifService) } + + private final case class IIIFServiceManagerImpl(iiifService: IIIFService) extends IIIFServiceManager { + + override def receive(message: IIIFRequest) = message match { + case req: GetFileMetadataRequest => iiifService.getFileMetadata(req) + case req: MoveTemporaryFileToPermanentStorageRequest => iiifService.moveTemporaryFileToPermanentStorage(req) + case req: DeleteTemporaryFileRequest => iiifService.deleteTemporaryFile(req) + case req: SipiGetTextFileRequest => iiifService.getTextFileRequest(req) + case IIIFServiceGetStatus => iiifService.getStatus() + case other => ZIO.logError(s"IIIFServiceManager received an unexpected message: $other") + } + } } diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala index f26d6a2daa..5c22a92dd1 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala @@ -1229,10 +1229,28 @@ class UsersADME2ESpec } "used to modify project admin group membership" should { - "add user to project admin group" in { + "add user to project admin group only if he is already member of that project" in { + // add user as project admin to images project - returns a BadRequest because user is not member of the project + val requestWithoutBeingMember = Post( + baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-admin-memberships/$imagesProjectIriEnc" + ) ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val responseWithoutBeingMember: HttpResponse = singleAwaitingRequest(requestWithoutBeingMember) + + assert(responseWithoutBeingMember.status === StatusCodes.BadRequest) + + // add user as member to images project + val requestAddUserToProject = Post( + baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-memberships/$imagesProjectIriEnc" + ) ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val responseAddUserToProject: HttpResponse = singleAwaitingRequest(requestAddUserToProject) + + assert(responseAddUserToProject.status === StatusCodes.OK) + + // verfiy that user is not yet project admin in images project val membershipsBeforeUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) membershipsBeforeUpdate should equal(Seq()) + // add user as project admin to images project val request = Post( baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-admin-memberships/$imagesProjectIriEnc" ) ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) @@ -1240,8 +1258,8 @@ class UsersADME2ESpec assert(response.status === StatusCodes.OK) + // verify that user has been added as project admin to images project val membershipsAfterUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) - membershipsAfterUpdate should equal(Seq(SharedTestDataADM.imagesProject)) clientTestDataCollector.addFile( @@ -1284,6 +1302,33 @@ class UsersADME2ESpec ) } + "remove user from project which also removes him from project admin group" in { + // add user as project admin to images project + val requestAddUserAsProjectAdmin = Post( + baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-admin-memberships/$imagesProjectIriEnc" + ) ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val responseAddUserAsProjectAdmin: HttpResponse = singleAwaitingRequest(requestAddUserAsProjectAdmin) + + assert(responseAddUserAsProjectAdmin.status === StatusCodes.OK) + + // verify that user has been added as project admin to images project + val membershipsBeforeUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) + membershipsBeforeUpdate should equal(Seq(SharedTestDataADM.imagesProject)) + + // remove user as project member from images project + val request = Delete( + baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-memberships/$imagesProjectIriEnc" + ) ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + assert(response.status === StatusCodes.OK) + + // verify that user has also been removed as project admin from images project + val projectAdminMembershipsAfterUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) + + projectAdminMembershipsAfterUpdate should equal(Seq()) + } + } "used to query group memberships" should { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala index 21f693ff95..c2e66f543e 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala @@ -633,13 +633,36 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica received.members.map(_.id) should contain(normalUser.id) } - "DELETE user from project" in { + "DELETE user from project and also as project admin" in { + // check project memberships (user should be member of images and incunabula projects) appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects.map(_.id).sorted should equal( Seq(imagesProject.id, incunabulaProject.id).sorted ) + // add user as project admin to images project + appActor ! UserProjectAdminMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + rootUser, + UUID.randomUUID() + ) + + expectMsgType[UserOperationResponseADM](timeout) + + // verify that the user has been added as project admin to the images project + appActor ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + rootUser, + UUID.randomUUID() + ) + val projectAdminMembershipsBeforeUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) + projectAdminMembershipsBeforeUpdate.projects.map(_.id).sorted should equal( + Seq(imagesProject.id).sorted + ) + + // remove the user as member of the images project appActor ! UserProjectMembershipRemoveRequestADM( normalUser.id, imagesProject.id, @@ -648,10 +671,21 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica ) expectMsgType[UserOperationResponseADM](timeout) + // verify that the user has been removed as project member of the images project appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) val membershipsAfterUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) membershipsAfterUpdate.projects should equal(Seq(incunabulaProject)) + // this should also have removed him as project admin from images project + appActor ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + rootUser, + UUID.randomUUID() + ) + val projectAdminMembershipsAfterUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) + projectAdminMembershipsAfterUpdate.projects should equal(Seq()) + + // also check that the user has been removed from the project's list of users appActor ! ProjectMembersGetRequestADM( ProjectIdentifierADM(maybeIri = Some(imagesProject.id)), requestingUser = rootUser @@ -659,6 +693,7 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica val received: ProjectMembersGetResponseADM = expectMsgType[ProjectMembersGetResponseADM](timeout) received.members should not contain normalUser.ofType(UserInformationTypeADM.Restricted) + } "return a 'ForbiddenException' if the user requesting update is not the project or system admin" in { @@ -694,7 +729,36 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica } "asked to update the user's project admin group membership" should { + + "Not ADD user to project admin group if he is not a member of that project" in { + // get the current project admin memberships (should be empty) + appActor ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + rootUser, + UUID.randomUUID() + ) + val membershipsBeforeUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) + membershipsBeforeUpdate.projects should equal(Seq()) + + // try to add user as project admin to images project (expected to fail because he is not a member of the project) + appActor ! UserProjectAdminMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + rootUser, + UUID.randomUUID() + ) + expectMsg( + timeout, + Failure( + BadRequestException( + "User http://rdfh.ch/users/normaluser is not a member of project http://rdfh.ch/projects/00FF. A user needs to be a member of the project to be added as project admin." + ) + ) + ) + } + "ADD user to project admin group" in { + // get the current project admin memberships (should be empty) appActor ! UserProjectAdminMembershipsGetRequestADM( normalUser.id, rootUser, @@ -703,14 +767,26 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica val membershipsBeforeUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects should equal(Seq()) + // add user as project member to images project + appActor ! UserProjectMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + rootUser, + UUID.randomUUID() + ) + expectMsgType[UserOperationResponseADM](timeout) + + // add user as project admin to images project appActor ! UserProjectAdminMembershipAddRequestADM( normalUser.id, imagesProject.id, rootUser, UUID.randomUUID() ) + expectMsgType[UserOperationResponseADM](timeout) + // get the updated project admin memberships (should contain images project) appActor ! UserProjectAdminMembershipsGetRequestADM( normalUser.id, rootUser, @@ -719,6 +795,7 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica val membershipsAfterUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsAfterUpdate.projects should equal(Seq(imagesProject)) + // get project admins for images project (should contain normal user) appActor ! ProjectAdminMembersGetRequestADM( ProjectIdentifierADM(maybeIri = Some(imagesProject.id)), requestingUser = rootUser