Skip to content

Commit

Permalink
refactor: Migrate DELETE /admin/groups/<groupIri> to Tapir (DEV-1588) (
Browse files Browse the repository at this point in the history
  • Loading branch information
mpro7 authored Mar 4, 2024
1 parent 3a21838 commit 2715aa1
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ~
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(_))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 2715aa1

Please sign in to comment.