From e0b143382fbf1c55146f775a720562d6ea7444d0 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 5 Oct 2022 17:08:21 +0200 Subject: [PATCH] fix: User can't be edited by project admin (DEV-1373) (#2232) --- Makefile | 46 ++++---- .../{wait-for-knora.sh => wait-for-api.sh} | 8 +- .../responders/admin/UsersResponderADM.scala | 34 +++--- .../knora/webapi/routing/HealthRoute.scala | 67 +++++------ .../admin/UsersResponderADMSpec.scala | 111 +++++++++++++++--- 5 files changed, 169 insertions(+), 97 deletions(-) rename webapi/scripts/{wait-for-knora.sh => wait-for-api.sh} (87%) diff --git a/Makefile b/Makefile index 0c0cd15866..af591d8462 100644 --- a/Makefile +++ b/Makefile @@ -72,74 +72,74 @@ docker-publish: docker-publish-dsp-api-image docker-publish-sipi-image ## publis ################################# .PHONY: print-env-file -print-env-file: ## prints the env file used by knora-stack +print-env-file: ## prints the env file used by dsp-stack @cat .env .PHONY: env-file -env-file: ## write the env file used by knora-stack. +env-file: ## write the env file used by dsp-stack. @echo DOCKERHOST=$(DOCKERHOST) > .env @echo KNORA_DB_REPOSITORY_NAME=$(KNORA_DB_REPOSITORY_NAME) >> .env @echo LOCAL_HOME=$(CURRENT_DIR) >> .env ################################# -## Knora Stack Targets +## DSP Stack Targets ################################# .PHONY: stack-up -stack-up: docker-build env-file ## starts the knora-stack: fuseki, sipi, api. +stack-up: docker-build env-file ## starts the dsp-stack: fuseki, sipi, api. @docker compose -f docker-compose.yml up -d db $(CURRENT_DIR)/webapi/scripts/wait-for-db.sh @docker compose -f docker-compose.yml up -d - $(CURRENT_DIR)/webapi/scripts/wait-for-knora.sh + $(CURRENT_DIR)/webapi/scripts/wait-for-api.sh .PHONY: stack-up-fast -stack-up-fast: docker-build-knora-api-image env-file ## starts the knora-stack by skipping rebuilding most of the images (only api image is rebuilt). +stack-up-fast: docker-build-knora-api-image env-file ## starts the dsp-stack by skipping rebuilding most of the images (only api image is rebuilt). docker-compose -f docker-compose.yml up -d .PHONY: stack-up-ci stack-up-ci: KNORA_DB_REPOSITORY_NAME := knora-test-unit -stack-up-ci: docker-build env-file print-env-file ## starts the knora-stack using 'knora-test-unit' repository: fuseki, sipi, api. +stack-up-ci: docker-build env-file print-env-file ## starts the dsp-stack using 'knora-test-unit' repository: fuseki, sipi, api. docker-compose -f docker-compose.yml up -d .PHONY: stack-restart -stack-restart: ## re-starts the knora-stack: fuseki, sipi, api. +stack-restart: ## re-starts the dsp-stack: fuseki, sipi, api. @docker compose -f docker-compose.yml down @docker compose -f docker-compose.yml up -d db $(CURRENT_DIR)/webapi/scripts/wait-for-db.sh @docker compose -f docker-compose.yml up -d - $(CURRENT_DIR)/webapi/scripts/wait-for-knora.sh + $(CURRENT_DIR)/webapi/scripts/wait-for-api.sh .PHONY: stack-restart-api stack-restart-api: ## re-starts the api. Usually used after loading data into fuseki. docker-compose -f docker-compose.yml restart api - @$(CURRENT_DIR)/webapi/scripts/wait-for-knora.sh + @$(CURRENT_DIR)/webapi/scripts/wait-for-api.sh .PHONY: stack-logs -stack-logs: ## prints out and follows the logs of the running knora-stack. +stack-logs: ## prints out and follows the logs of the running dsp-stack. @docker compose -f docker-compose.yml logs -f .PHONY: stack-logs-db -stack-logs-db: ## prints out and follows the logs of the 'db' container running in knora-stack. +stack-logs-db: ## prints out and follows the logs of the 'db' container running in dsp-stack. @docker compose -f docker-compose.yml logs -f db .PHONY: stack-logs-db-no-follow -stack-logs-db-no-follow: ## prints out the logs of the 'db' container running in knora-stack. +stack-logs-db-no-follow: ## prints out the logs of the 'db' container running in dsp-stack. @docker-compose -f docker-compose.yml logs db .PHONY: stack-logs-sipi -stack-logs-sipi: ## prints out and follows the logs of the 'sipi' container running in knora-stack. +stack-logs-sipi: ## prints out and follows the logs of the 'sipi' container running in dsp-stack. @docker compose -f docker-compose.yml logs -f sipi .PHONY: stack-logs-sipi-no-follow -stack-logs-sipi-no-follow: ## prints out the logs of the 'sipi' container running in knora-stack. +stack-logs-sipi-no-follow: ## prints out the logs of the 'sipi' container running in dsp-stack. @docker compose -f docker-compose.yml logs sipi .PHONY: stack-logs-api -stack-logs-api: ## prints out and follows the logs of the 'api' container running in knora-stack. +stack-logs-api: ## prints out and follows the logs of the 'api' container running in dsp-stack. @docker compose -f docker-compose.yml logs -f api .PHONY: stack-logs-api-no-follow -stack-logs-api-no-follow: ## prints out the logs of the 'api' container running in knora-stack. +stack-logs-api-no-follow: ## prints out the logs of the 'api' container running in dsp-stack. @docker compose -f docker-compose.yml logs api .PHONY: stack-health @@ -151,11 +151,11 @@ stack-status: @docker compose -f docker-compose.yml ps .PHONY: stack-down -stack-down: ## stops the knora-stack. +stack-down: ## stops the dsp-stack. @docker compose -f docker-compose.yml down .PHONY: stack-down-delete-volumes -stack-down-delete-volumes: ## stops the knora-stack and deletes any created volumes (deletes the database!). +stack-down-delete-volumes: ## stops the dsp-stack and deletes any created volumes (deletes the database!). @docker compose -f docker-compose.yml down --volumes .PHONY: stack-config @@ -164,11 +164,11 @@ stack-config: env-file ## stack without api .PHONY: stack-without-api -stack-without-api: stack-up ## starts the knora-stack without knora-api: fuseki and sipi only. +stack-without-api: stack-up ## starts the dsp-stack without dsp-api: fuseki and sipi only. @docker compose -f docker-compose.yml stop api .PHONY: stack-without-api-and-sipi -stack-without-api-and-sipi: stack-up ## starts the knora-stack without knora-api and sipi: fuseki only. +stack-without-api-and-sipi: stack-up ## starts the dsp-stack without dsp-api and sipi: fuseki only. @docker compose -f docker-compose.yml stop api @docker compose -f docker-compose.yml stop sipi @@ -348,11 +348,11 @@ clean-sipi-projects: ## deletes all files uploaded within a project @rm -rf sipi/images/originals/[0-9A-F][0-9A-F][0-9A-F][0-9A-F] .PHONY: check -check: ## Run code formating check +check: ## Run code formatting check @sbt "check" .PHONY: fmt -fmt: ## Run code formating fix +fmt: ## Run code formatting fix @sbt "fmt" diff --git a/webapi/scripts/wait-for-knora.sh b/webapi/scripts/wait-for-api.sh similarity index 87% rename from webapi/scripts/wait-for-knora.sh rename to webapi/scripts/wait-for-api.sh index 406dc189f0..0f143fa934 100755 --- a/webapi/scripts/wait-for-knora.sh +++ b/webapi/scripts/wait-for-api.sh @@ -37,11 +37,11 @@ if [[ -z "${TIMEOUT}" ]]; then TIMEOUT=360 fi -poll-knora() { +check-health() { STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://${HOST}/health) if [ "${STATUS}" -eq 200 ]; then - echo "Knora started" + echo "==> DSP-API started" return 0 else return 1 @@ -50,9 +50,9 @@ poll-knora() { attempt_counter=0 -until poll-knora; do +until check-health; do if [ ${attempt_counter} -eq ${TIMEOUT} ]; then - echo "Timed out waiting for Knora to start" + echo "==> Timed out waiting for DSP-API to start" exit 1 fi 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 87328230e6..8e288a3802 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 @@ -755,13 +755,13 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } // create the update request - updateUseResult <- updateUserADM( - userIri = userIri, - userUpdatePayload = UserChangeRequestADM(projects = Some(updatedProjectMembershipIris)), - requestingUser = requestingUser, - apiRequestID = apiRequestID - ) - } yield updateUseResult + updateUserResult <- updateUserADM( + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(projects = Some(updatedProjectMembershipIris)), + requestingUser = requestingUser, + apiRequestID = apiRequestID + ) + } yield updateUserResult for { // run the task with an IRI lock @@ -1467,7 +1467,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde /* Verify that the user was updated */ maybeUpdatedUserADM <- getSingleUserADM( identifier = UserIdentifierADM(maybeIri = Some(userIri)), - requestingUser = requestingUser, + requestingUser = KnoraSystemInstances.Users.SystemUser, userInformationType = UserInformationTypeADM.Full, skipCache = true ) @@ -1516,12 +1516,18 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } _ = if (userUpdatePayload.projects.isDefined) { - - if (updatedUserADM.projects.map(_.id).sorted != userUpdatePayload.projects.get.sorted) { - throw UpdateNotPerformedException( - "User's 'project' memberships were not updated. Please report this as a possible bug." - ) - } + for { + projects <- userProjectMembershipsGetADM( + userIri = userIri, + requestingUser = requestingUser + ) + _ = + if (projects.map(_.id).sorted != userUpdatePayload.projects.get.sorted) { + throw UpdateNotPerformedException( + "User's 'project' memberships were not updated. Please report this as a possible bug." + ) + } + } yield UserProjectMembershipsGetResponseADM(projects) } _ = if (userUpdatePayload.systemAdmin.isDefined) { 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 9bce509696..039897bd59 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala @@ -27,48 +27,35 @@ trait HealthCheck { for { _ <- ZIO.logInfo("get application state") state <- state.get - result <- createResult(state) + result <- setHealthState(state) + _ <- ZIO.logInfo("set health state") response <- createResponse(result) _ <- ZIO.logInfo("getting application state done") } yield response - private def createResult(state: AppState): UIO[HealthCheckResult] = - ZIO - .attempt( - state match { - case AppState.Stopped => unhealthy("Stopped. Please retry later.") - case AppState.StartingUp => unhealthy("Starting up. Please retry later.") - case AppState.WaitingForTriplestore => - unhealthy("Waiting for triplestore. Please retry later.") - case AppState.TriplestoreReady => - unhealthy("Triplestore ready. Please retry later.") - case AppState.UpdatingRepository => - unhealthy("Updating repository. Please retry later.") - case AppState.RepositoryUpToDate => - unhealthy("Repository up to date. Please retry later.") - case AppState.CreatingCaches => unhealthy("Creating caches. Please retry later.") - case AppState.CachesReady => unhealthy("Caches ready. Please retry later.") - case AppState.UpdatingSearchIndex => - unhealthy("Updating search index. Please retry later.") - case AppState.SearchIndexReady => - unhealthy("Search index ready. Please retry later.") - case AppState.LoadingOntologies => - unhealthy("Loading ontologies. Please retry later.") - case AppState.OntologiesReady => unhealthy("Ontologies ready. Please retry later.") - case AppState.WaitingForIIIFService => - unhealthy("Waiting for IIIF service. Please retry later.") - case AppState.IIIFServiceReady => - unhealthy("IIIF service ready. Please retry later.") - case AppState.WaitingForCacheService => - unhealthy("Waiting for cache service. Please retry later.") - case AppState.CacheServiceReady => - unhealthy("Cache service ready. Please retry later.") - case AppState.MaintenanceMode => - unhealthy("Application is in maintenance mode. Please retry later.") - case AppState.Running => healthy - } - ) - .orDie + private def setHealthState(state: AppState): UIO[HealthCheckResult] = + ZIO.succeed( + state match { + case AppState.Stopped => unhealthy("Stopped. Please retry later.") + case AppState.StartingUp => unhealthy("Starting up. Please retry later.") + case AppState.WaitingForTriplestore => unhealthy("Waiting for triplestore. Please retry later.") + case AppState.TriplestoreReady => unhealthy("Triplestore ready. Please retry later.") + case AppState.UpdatingRepository => unhealthy("Updating repository. Please retry later.") + case AppState.RepositoryUpToDate => unhealthy("Repository up to date. Please retry later.") + case AppState.CreatingCaches => unhealthy("Creating caches. Please retry later.") + case AppState.CachesReady => unhealthy("Caches ready. Please retry later.") + case AppState.UpdatingSearchIndex => unhealthy("Updating search index. Please retry later.") + case AppState.SearchIndexReady => unhealthy("Search index ready. Please retry later.") + case AppState.LoadingOntologies => unhealthy("Loading ontologies. Please retry later.") + case AppState.OntologiesReady => unhealthy("Ontologies ready. Please retry later.") + case AppState.WaitingForIIIFService => unhealthy("Waiting for IIIF service. Please retry later.") + case AppState.IIIFServiceReady => unhealthy("IIIF service ready. Please retry later.") + case AppState.WaitingForCacheService => unhealthy("Waiting for cache service. Please retry later.") + case AppState.CacheServiceReady => unhealthy("Cache service ready. Please retry later.") + case AppState.MaintenanceMode => unhealthy("Application is in maintenance mode. Please retry later.") + case AppState.Running => healthy + } + ) private def createResponse(result: HealthCheckResult): UIO[HttpResponse] = ZIO @@ -94,12 +81,12 @@ trait HealthCheck { private case class HealthCheckResult(name: String, severity: String, status: Boolean, message: String) - private def unhealthy(str: String) = + private def unhealthy(message: String) = HealthCheckResult( name = "AppState", severity = "non fatal", status = false, - message = str + message = message ) private val healthy = 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 2b48b555d3..21f693ff95 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 @@ -21,10 +21,14 @@ import org.knora.webapi._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.groupsmessages.GroupMembersGetRequestADM import org.knora.webapi.messages.admin.responder.groupsmessages.GroupMembersGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages._ +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectAdminMembersGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectAdminMembersGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectMembersGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectMembersGetResponseADM import org.knora.webapi.messages.admin.responder.usersmessages._ import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2 import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -39,9 +43,10 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica private val anythingAdminUser = SharedTestDataADM.anythingAdminUser private val normalUser = SharedTestDataADM.normalUser - private val incunabulaUser = SharedTestDataADM.incunabulaProjectAdminUser + private val incunabulaProjectAdminUser = SharedTestDataADM.incunabulaProjectAdminUser private val imagesProject = SharedTestDataADM.imagesProject + private val incunabulaProject = SharedTestDataADM.incunabulaProject private val imagesReviewerGroup = SharedTestDataADM.imagesReviewerGroup implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -97,11 +102,11 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica "return a profile if the user (incunabula user) is known" in { appActor ! UserGetADM( - identifier = UserIdentifierADM(maybeIri = Some(incunabulaUser.id)), + identifier = UserIdentifierADM(maybeIri = Some(incunabulaProjectAdminUser.id)), userInformationTypeADM = UserInformationTypeADM.Full, requestingUser = KnoraSystemInstances.Users.SystemUser ) - expectMsg(Some(incunabulaUser.ofType(UserInformationTypeADM.Full))) + expectMsg(Some(incunabulaProjectAdminUser.ofType(UserInformationTypeADM.Full))) } "return 'NotFoundException' when the user is unknown" in { @@ -135,11 +140,11 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica "return a profile if the user (incunabula user) is known" in { appActor ! UserGetADM( - identifier = UserIdentifierADM(maybeEmail = Some(incunabulaUser.email)), + identifier = UserIdentifierADM(maybeEmail = Some(incunabulaProjectAdminUser.email)), userInformationTypeADM = UserInformationTypeADM.Full, requestingUser = KnoraSystemInstances.Users.SystemUser ) - expectMsg(Some(incunabulaUser.ofType(UserInformationTypeADM.Full))) + expectMsg(Some(incunabulaProjectAdminUser.ofType(UserInformationTypeADM.Full))) } "return 'NotFoundException' when the user is unknown" in { @@ -173,11 +178,11 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica "return a profile if the user (incunabula user) is known" in { appActor ! UserGetADM( - identifier = UserIdentifierADM(maybeUsername = Some(incunabulaUser.username)), + identifier = UserIdentifierADM(maybeUsername = Some(incunabulaProjectAdminUser.username)), userInformationTypeADM = UserInformationTypeADM.Full, requestingUser = KnoraSystemInstances.Users.SystemUser ) - expectMsg(Some(incunabulaUser.ofType(UserInformationTypeADM.Full))) + expectMsg(Some(incunabulaProjectAdminUser.ofType(UserInformationTypeADM.Full))) } "return 'NotFoundException' when the user is unknown" in { @@ -359,8 +364,10 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica // need to be able to authenticate credentials with new password val resF = Authenticator.authenticateCredentialsV2( - credentials = - Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test123456")), + credentials = Some( + KnoraCredentialsV2 + .KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test123456") + ), appConfig )(system, appActor, executionContext) @@ -387,8 +394,10 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica // need to be able to authenticate credentials with new password val resF = Authenticator.authenticateCredentialsV2( - credentials = - Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test654321")), + credentials = Some( + KnoraCredentialsV2 + .KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test654321") + ), appConfig )(system, appActor, executionContext) @@ -556,10 +565,80 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica received.members.map(_.id) should contain(normalUser.id) } + "not ADD user to project as project admin of another project" in { + // get current project memberships + appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) + val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) + membershipsBeforeUpdate.projects.map(_.id).sorted should equal(Seq(imagesProject.id).sorted) + + // try to add user to incunabula project but as project admin of another project + appActor ! UserProjectMembershipAddRequestADM( + normalUser.id, + incunabulaProject.id, + anythingAdminUser, + UUID.randomUUID() + ) + + expectMsg( + timeout, + Failure( + ForbiddenException("User's project membership can only be changed by a project or system administrator") + ) + ) + + // check that the user is still only member of one project + appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) + + val membershipsAfterUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) + membershipsAfterUpdate.projects.map(_.id).sorted should equal(Seq(imagesProject.id).sorted) + + // check that the user was not added to the project + appActor ! ProjectMembersGetRequestADM( + ProjectIdentifierADM(maybeIri = Some(incunabulaProject.id)), + requestingUser = KnoraSystemInstances.Users.SystemUser + ) + val received = expectMsgType[ProjectMembersGetResponseADM](timeout) + + received.members.map(_.id) should not contain (normalUser.id) + } + + "ADD user to project as project admin" in { + // get current project memberships + appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) + val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) + membershipsBeforeUpdate.projects.map(_.id).sorted should equal(Seq(imagesProject.id).sorted) + + // add user to images project (00FF) + appActor ! UserProjectMembershipAddRequestADM( + normalUser.id, + incunabulaProject.id, + incunabulaProjectAdminUser, + UUID.randomUUID() + ) + + val membershipUpdateResponse = expectMsgType[UserOperationResponseADM](timeout) + + appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) + val membershipsAfterUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) + membershipsAfterUpdate.projects.map(_.id).sorted should equal( + Seq(imagesProject.id, incunabulaProject.id).sorted + ) + + appActor ! ProjectMembersGetRequestADM( + ProjectIdentifierADM(maybeIri = Some(incunabulaProject.id)), + requestingUser = KnoraSystemInstances.Users.SystemUser + ) + val received = expectMsgType[ProjectMembersGetResponseADM](timeout) + + received.members.map(_.id) should contain(normalUser.id) + } + "DELETE user from project" in { appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) - membershipsBeforeUpdate.projects should equal(Seq(imagesProject)) + membershipsBeforeUpdate.projects.map(_.id).sorted should equal( + Seq(imagesProject.id, incunabulaProject.id).sorted + ) appActor ! UserProjectMembershipRemoveRequestADM( normalUser.id, @@ -571,7 +650,7 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica appActor ! UserProjectMembershipsGetRequestADM(normalUser.id, rootUser) val membershipsAfterUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) - membershipsAfterUpdate.projects should equal(Seq()) + membershipsAfterUpdate.projects should equal(Seq(incunabulaProject)) appActor ! ProjectMembersGetRequestADM( ProjectIdentifierADM(maybeIri = Some(imagesProject.id)), @@ -646,7 +725,7 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica ) val received: ProjectAdminMembersGetResponseADM = expectMsgType[ProjectAdminMembersGetResponseADM](timeout) - received.members should contain(normalUser.ofType(UserInformationTypeADM.Restricted)) + received.members.map(_.id) should contain(normalUser.id) } "DELETE user from project admin group" in {