diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala index d78cd1be85..966ff9bc00 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala @@ -10,7 +10,6 @@ import java.util.UUID import dsp.errors.* import dsp.valueobjects.V2 import org.knora.webapi.* -import org.knora.webapi.messages.admin.responder.groupsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.* import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.routing.UnsafeZioRun @@ -189,7 +188,17 @@ class GroupsResponderADMSpec extends CoreSpec { } "return 'BadRequest' if nothing would be changed during the update" in { - an[BadRequestException] should be thrownBy ChangeGroupApiRequestADM(None, None, None, None) + val exit = UnsafeZioRun.run( + GroupsResponderADM.updateGroup( + GroupIri.unsafeFrom(newGroupIri.get), + GroupUpdateRequest(None, None, None, None), + UUID.randomUUID + ) + ) + assertFailsWithA[BadRequestException]( + exit, + "No data would be changed. Aborting update request." + ) } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala index 615818cc7b..30cf10c51b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala @@ -6,14 +6,9 @@ package org.knora.webapi.messages.admin.responder.groupsmessages import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.DefaultJsonProtocol -import spray.json.JsValue import spray.json.JsonFormat import spray.json.RootJsonFormat -import java.util.UUID - -import dsp.errors.BadRequestException -import dsp.valueobjects.V2 import org.knora.webapi.IRI import org.knora.webapi.core.RelayedMessage import org.knora.webapi.messages.ResponderRequest.KnoraRequestADM @@ -24,52 +19,6 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.User -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// API requests - -/** - * Represents an API request payload that asks the Knora API server to update - * an existing group. There are two change cases that are covered with this - * data structure: - * (1) change of name, descriptions, and selfjoin - * (2) change of status - * - * @param name the new group's name. - * @param descriptions the new group's descriptions. - * @param status the new group's status. - * @param selfjoin the new group's self-join status. - */ -case class ChangeGroupApiRequestADM( - name: Option[String] = None, - descriptions: Option[Seq[V2.StringLiteralV2]] = None, - status: Option[Boolean] = None, - selfjoin: Option[Boolean] = None -) extends GroupsADMJsonProtocol { -// TODO-mpro: once status is separate route then it can be removed - private val parametersCount = List( - name, - descriptions, - status, - selfjoin - ).flatten.size - - // something needs to be sent, i.e. everything 'None' is not allowed - if (parametersCount == 0) throw BadRequestException("No data sent in API request.") - - /** - * check that only allowed information for the 2 cases is sent and not more. - */ - // change status case - if (status.isDefined) { - if (parametersCount > 1) throw BadRequestException("Too many parameters sent for group status change.") - } - - // change basic group information case - if (parametersCount > 3) throw BadRequestException("Too many parameters sent for basic group information change.") - - def toJsValue: JsValue = changeGroupApiRequestADMFormat.write(this) -} - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Messages @@ -109,21 +58,6 @@ case class MultipleGroupsGetRequestADM( */ case class GroupMembersGetRequestADM(groupIri: IRI, requestingUser: User) extends GroupsResponderRequestADM -/** - * Request changing the status (active/inactive) of an existing group. - * - * @param groupIri the IRI of the group to be deleted. - * @param changeGroupRequest the data which needs to be update. - * @param requestingUser the user initiating the request. - * @param apiRequestID the ID of the API request. - */ -case class GroupChangeStatusRequestADM( - groupIri: IRI, - changeGroupRequest: ChangeGroupApiRequestADM, - requestingUser: User, - apiRequestID: UUID -) extends GroupsResponderRequestADM - // Responses /** * Represents the Knora API v1 JSON response to a request for information about all groups. @@ -184,6 +118,4 @@ trait GroupsADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol wi implicit val groupsGetResponseADMFormat: RootJsonFormat[GroupsGetResponseADM] = jsonFormat(GroupsGetResponseADM, "groups") implicit val groupResponseADMFormat: RootJsonFormat[GroupGetResponseADM] = jsonFormat(GroupGetResponseADM, "group") - implicit val changeGroupApiRequestADMFormat: RootJsonFormat[ChangeGroupApiRequestADM] = - jsonFormat(ChangeGroupApiRequestADM, "name", "descriptions", "status", "selfjoin") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala index 04211bdbb0..5a4e189bb8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala @@ -129,21 +129,16 @@ trait GroupsResponderADM { ): Task[GroupGetResponseADM] /** - * Change group's basic information. + * Delete a group by changing its status to 'false'. * - * @param groupIri the IRI of the group we want to change. - * @param changeGroupRequest the change request. - * @param requestingUser the user making the request. + * @param iri the IRI of the group to be deleted. * @param apiRequestID the unique request ID. * @return a [[GroupGetResponseADM]]. */ - def changeGroupStatusRequestADM( - groupIri: IRI, - changeGroupRequest: ChangeGroupApiRequestADM, - requestingUser: User, + def deleteGroup( + iri: GroupIri, apiRequestID: UUID ): Task[GroupGetResponseADM] - } final case class GroupsResponderADMLive( @@ -167,14 +162,7 @@ final case class GroupsResponderADMLive( case r: GroupGetADM => groupGetADM(r.groupIri) case r: MultipleGroupsGetRequestADM => multipleGroupsGetRequestADM(r.groupIris) case r: GroupMembersGetRequestADM => groupMembersGetRequestADM(r.groupIri, r.requestingUser) - case r: GroupChangeStatusRequestADM => - changeGroupStatusRequestADM( - r.groupIri, - r.changeGroupRequest, - r.requestingUser, - r.apiRequestID - ) - case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) + case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) } /** @@ -406,62 +394,15 @@ final case class GroupsResponderADMLive( IriLocker.runWithIriLock(apiRequestID, groupIri.value, task) } - /** - * Change group's basic information. - * - * @param groupIri the IRI of the group we want to change. - * @param changeGroupRequest the change request. - * @param requestingUser the user making the request. - * @param apiRequestID the unique request ID. - * @return a [[GroupGetResponseADM]]. - */ - override def changeGroupStatusRequestADM( - groupIri: IRI, - changeGroupRequest: ChangeGroupApiRequestADM, - requestingUser: User, + override def deleteGroup( + iri: GroupIri, apiRequestID: UUID ): Task[GroupGetResponseADM] = { - - /** - * The actual change group task run with an IRI lock. - */ - def changeGroupStatusTask( - groupIri: IRI, - changeGroupRequest: ChangeGroupApiRequestADM, - requestingUser: User - ): Task[GroupGetResponseADM] = - for { - /* Get the project IRI which also verifies that the group exists. */ - groupADM <- groupGetADM(groupIri) - .someOrFail(NotFoundException(s"Group <$groupIri> not found. Aborting update request.")) - - /* check if the requesting user is allowed to perform updates */ - _ <- ZIO - .fail(ForbiddenException("Group's status can only be changed by a project or system admin.")) - .when { - val userPermissions = requestingUser.permissions - !userPermissions.isProjectAdmin(groupADM.project.id) && - !userPermissions.isSystemAdmin - } - - maybeStatus = changeGroupRequest.status.map(GroupStatus.from) - - /* create the update request */ - groupUpdatePayload = GroupUpdateRequest(status = maybeStatus) - - iri <- ZIO.fromEither(GroupIri.from(groupIri)).mapError(BadRequestException(_)) - - // update group status - updateGroupResult <- updateGroupHelper(iri, groupUpdatePayload) - - // remove all members from group if status is false - operationResponse <- - removeGroupMembersIfNecessary(changedGroup = updateGroupResult.group) - - } yield operationResponse - - val task = changeGroupStatusTask(groupIri, changeGroupRequest, requestingUser) - IriLocker.runWithIriLock(apiRequestID, groupIri, task) + val task = for { + updated <- updateGroupHelper(iri, GroupUpdateRequest(None, None, Some(GroupStatus.inactive), None)) + result <- removeGroupMembersIfNecessary(updated.group) + } yield result + IriLocker.runWithIriLock(apiRequestID, iri.value, task) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index 0cbfbbbbe2..dc875e69ad 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -24,7 +24,6 @@ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.routing -import org.knora.webapi.routing.admin.* import org.knora.webapi.routing.v2.* import org.knora.webapi.slice.admin.api.AdminApiRoutes import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler @@ -101,7 +100,6 @@ private final case class ApiRoutesImpl( DSPApiDirectives.handleErrors(appConfig) { (adminApiRoutes.routes ++ resourceInfoRoutes.routes ++ searchApiRoutes.routes).reduce(_ ~ _) ~ AuthenticationRouteV2().makeRoute ~ - GroupsRouteADM(routeData, runtime).makeRoute ~ HealthRoute().makeRoute ~ ListsRouteV2().makeRoute ~ OntologiesRouteV2().makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala deleted file mode 100644 index c7370a24d8..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.routing.admin - -import org.apache.pekko.http.scaladsl.server.Directives.* -import org.apache.pekko.http.scaladsl.server.PathMatcher -import org.apache.pekko.http.scaladsl.server.Route -import zio.* - -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.admin.responder.groupsmessages.* -import org.knora.webapi.routing.Authenticator -import org.knora.webapi.routing.KnoraRoute -import org.knora.webapi.routing.KnoraRouteData -import org.knora.webapi.routing.RouteUtilADM.* - -/** - * Provides a routing function for API routes that deal with groups. - */ - -final case class GroupsRouteADM( - private val routeData: KnoraRouteData, - override protected implicit val runtime: Runtime[Authenticator & StringFormatter & MessageRelay] -) extends KnoraRoute(routeData, runtime) - with GroupsADMJsonProtocol { - - private val groupsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "groups") - - override def makeRoute: Route = - deleteGroup() - - /** - * Deletes a group (sets status to false). - */ - private def deleteGroup(): Route = path(groupsBasePath / Segment) { groupIri => - delete { ctx => - val task = for { - r <- getIriUserUuid(groupIri, ctx) - changeStatus = ChangeGroupApiRequestADM(status = Some(false)) - } yield GroupChangeStatusRequestADM(r.iri, changeStatus, r.user, r.uuid) - runJsonRouteZ(task, ctx) - } - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala index 7262696635..b0f5b320ee 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala @@ -65,7 +65,12 @@ final case class GroupsEndpoints(baseEndpoints: BaseEndpoints) { .out(sprayJsonBody[GroupGetResponseADM]) .description("Updates a group's status.") - private val securedEndpoins = Seq(getGroupMembers, postGroup, putGroup).map(_.endpoint) + val deleteGroup = baseEndpoints.securedEndpoint.delete + .in(base / groupIriPathVar) + .out(sprayJsonBody[GroupGetResponseADM]) + .description("Deletes a group by changing its status to 'false'.") + + private val securedEndpoins = Seq(getGroupMembers, postGroup, putGroup, deleteGroup).map(_.endpoint) val endpoints: Seq[AnyEndpoint] = (Seq(getGroups, getGroupByIri) ++ securedEndpoins) .map(_.tag("Admin Groups")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala index 9e93d3fb58..dc36fc936e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala @@ -61,8 +61,14 @@ case class GroupsEndpointsHandler( } ) + private val deleteGroupHandler = + SecuredEndpointHandler( + endpoints.deleteGroup, + user => iri => restService.deleteGroup(iri, user) + ) + private val securedHandlers = - List(getGroupMembersHandler, postGroupHandler, putGroupHandler, putGroupStatusHandler) + List(getGroupMembersHandler, postGroupHandler, putGroupHandler, putGroupStatusHandler, deleteGroupHandler) .map(mapper.mapSecuredEndpointHandler(_)) val allHandlers = List(getGroupsHandler, getGroupByIriHandler).map(mapper.mapPublicEndpointHandler(_)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupsRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupsRestService.scala index 7bf564c47f..b92828047c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupsRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupsRestService.scala @@ -28,6 +28,7 @@ trait GroupsRestService { def postGroup(request: GroupCreateRequest, user: User): Task[GroupGetResponseADM] def putGroup(iri: GroupIri, request: GroupUpdateRequest, user: User): Task[GroupGetResponseADM] def putGroupStatus(iri: GroupIri, request: GroupStatusUpdateRequest, user: User): Task[GroupGetResponseADM] + def deleteGroup(iri: GroupIri, user: User): Task[GroupGetResponseADM] } final case class GroupsRestServiceLive( @@ -78,6 +79,14 @@ final case class GroupsRestServiceLive( internal <- responder.updateGroupStatus(iri, request, uuid) external <- format.toExternal(internal) } yield external + + override def deleteGroup(iri: GroupIri, user: User): Task[GroupGetResponseADM] = + for { + _ <- auth.ensureSystemAdminOrProjectAdminOfGroup(user, iri) + uuid <- Random.nextUUID + internal <- responder.deleteGroup(iri, uuid) + external <- format.toExternal(internal) + } yield external } object GroupsRestServiceLive {