Skip to content

Commit

Permalink
refactor: Add caching to KnoraGroupRepo (DEV-3311) (#3204)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcin Procyk <marcin.procyk@dasch.swiss>
  • Loading branch information
seakayone and mpro7 authored Apr 23, 2024
1 parent fc85b24 commit 57a3a06
Show file tree
Hide file tree
Showing 13 changed files with 34 additions and 314 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ package org.knora.webapi.responders.admin
import com.typesafe.scalalogging.LazyLogging
import zio._

import java.util.UUID

import dsp.errors._
import org.knora.webapi._
import org.knora.webapi.core.MessageHandler
Expand All @@ -25,14 +23,11 @@ import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstru
import org.knora.webapi.messages.store.triplestoremessages._
import org.knora.webapi.messages.twirl.queries.sparql
import org.knora.webapi.messages.util.KnoraSystemInstances
import org.knora.webapi.responders.IriLocker
import org.knora.webapi.responders.IriService
import org.knora.webapi.responders.Responder
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest
import org.knora.webapi.slice.admin.domain.model
import org.knora.webapi.slice.admin.domain.model.Group
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.GroupStatus
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.model.UserIri
import org.knora.webapi.slice.admin.domain.service.KnoraUserService
Expand Down Expand Up @@ -179,111 +174,6 @@ final case class GroupsResponderADM(
*/
def groupMembersGetRequest(iri: GroupIri, user: User): Task[GroupMembersGetResponseADM] =
groupMembersGetADM(iri.value, user).map(GroupMembersGetResponseADM.apply)

def deleteGroup(
iri: GroupIri,
apiRequestID: UUID,
): Task[GroupGetResponseADM] = {
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)
}

/**
* Main group update method.
*
* @param groupIri the IRI of the group we are updating.
* @param request the payload holding the information which we want to update.
* @return a [[GroupGetResponseADM]]
*/
private def updateGroupHelper(groupIri: GroupIri, request: GroupUpdateRequest) =
for {
_ <- ZIO
.fail(BadRequestException("No data would be changed. Aborting update request."))
.when(
// parameter list is empty
List(
request.name,
request.descriptions,
request.status,
request.selfjoin,
).flatten.isEmpty,
)

/* Verify that the group exists. */
groupADM <-
groupGetADM(groupIri.value)
.someOrFail(NotFoundException(s"Group <${groupIri.value}> not found. Aborting update request."))

/* Verify that the potentially new name is unique */
groupByNameAlreadyExists <- (
for {
name <- request.name
project <- groupADM.project
} yield groupByNameAndProjectExists(name.value, project.id)
).getOrElse(ZIO.succeed(false))
_ <- ZIO
.fail(BadRequestException(s"Group with name: '${request.name.get.value}' already exists."))
.when(groupByNameAlreadyExists)

/* Update group */
updateGroupSparqlString =
sparql.admin.txt
.updateGroup(
adminNamedGraphIri = "http://www.knora.org/data/admin",
groupIri.value,
maybeName = request.name.map(_.value),
maybeDescriptions = request.descriptions.map(_.value),
maybeProject = None, // maybe later we want to allow moving of a group to another project
maybeStatus = request.status.map(_.value),
maybeSelfjoin = request.selfjoin.map(_.value),
)
_ <- triplestore.query(Update(updateGroupSparqlString))

/* Verify that the project was updated. */
updatedGroup <-
groupGetADM(groupIri.value)
.someOrFail(UpdateNotPerformedException("Group was not updated. Please report this as a possible bug."))
} yield GroupGetResponseADM(updatedGroup)

////////////////////
// Helper Methods //
////////////////////

/**
* Helper method for checking if a group identified by name / project IRI exists.
*
* @param name the name of the group.
* @param projectIri the IRI of the project.
* @return a [[Boolean]].
*/
private def groupByNameAndProjectExists(name: String, projectIri: IRI): Task[Boolean] =
triplestore.query(Ask(sparql.admin.txt.checkGroupExistsByName(projectIri, name)))

/**
* In the case that the group was deactivated (status = false), the
* group members need to be removed from the group.
*
* @param changedGroup the group with the new status.
* @return a [[GroupGetResponseADM]]
*/
private def removeGroupMembersIfNecessary(changedGroup: Group) =
if (changedGroup.status) {
// group active. no need to remove members.
logger.debug("removeGroupMembersIfNecessary - group active. no need to remove members.")
ZIO.succeed(GroupGetResponseADM(changedGroup))
} else {
// group deactivated. need to remove members.
logger.debug("removeGroupMembersIfNecessary - group deactivated. need to remove members.")
for {
members <- groupMembersGetADM(changedGroup.id, KnoraSystemInstances.Users.SystemUser)
_ <- ZIO.foreachDiscard(members)(user =>
knoraUserService.removeUserFromGroup(user, changedGroup).mapError(BadRequestException.apply),
)
} yield GroupGetResponseADM(group = changedGroup)
}
}

object GroupsResponderADM {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,12 @@ final case class AuthenticatorLive(
case Some(KnoraJWTTokenCredentialsV2(jwtToken)) =>
ZIO
.fail(BadCredentialsException(BAD_CRED_NOT_VALID))
.whenZIO(jwtService.validateToken(jwtToken).map(!_))
.unless(jwtService.isTokenValid(jwtToken))
.as(true)
case Some(KnoraSessionCredentialsV2(sessionToken)) =>
ZIO
.fail(BadCredentialsException(BAD_CRED_NOT_VALID))
.whenZIO(
jwtService.validateToken(sessionToken).map(!_),
)
.unless(jwtService.isTokenValid(sessionToken))
.as(true)
case None =>
ZIO.fail(BadCredentialsException(BAD_CRED_NONE_SUPPLIED))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.knora.webapi.slice.admin.api.service.StoreRestService
import org.knora.webapi.slice.admin.api.service.UserRestService
import org.knora.webapi.slice.admin.domain.service.AdministrativePermissionService
import org.knora.webapi.slice.admin.domain.service.GroupService
import org.knora.webapi.slice.admin.domain.service.KnoraGroupService
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.admin.domain.service.KnoraUserService
import org.knora.webapi.slice.admin.domain.service.KnoraUserToUserConverter
Expand All @@ -47,6 +48,7 @@ object AdminApiModule {
GroupsResponderADM &
GroupService &
HandlerMapper &
KnoraGroupService &
KnoraProjectService &
KnoraResponseRenderer &
KnoraUserService &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.GroupStatus
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.GroupService
import org.knora.webapi.slice.admin.domain.service.KnoraGroupService
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.common.api.AuthorizationRestService
import org.knora.webapi.slice.common.api.KnoraResponseRenderer
Expand All @@ -26,6 +28,7 @@ final case class GroupRestService(
auth: AuthorizationRestService,
format: KnoraResponseRenderer,
groupService: GroupService,
knoraGroupService: KnoraGroupService,
knoraProjectService: KnoraProjectService,
responder: GroupsResponderADM,
) {
Expand Down Expand Up @@ -74,21 +77,18 @@ final case class GroupRestService(
} yield external

def putGroupStatus(iri: GroupIri, request: GroupStatusUpdateRequest, user: User): Task[GroupGetResponseADM] =
for {
_ <- auth.ensureSystemAdminOrProjectAdminOfGroup(user, iri)
groupToUpdate <- groupService
.findById(iri)
.someOrFail(NotFoundException(s"Group <${iri.value}> not found."))
internal <- groupService.updateGroupStatus(groupToUpdate, request.status).map(GroupGetResponseADM.apply)
external <- format.toExternalADM(internal)
} yield external
updateStatus(iri, request.status, user)

def deleteGroup(iri: GroupIri, user: User): Task[GroupGetResponseADM] =
updateStatus(iri, GroupStatus.inactive, user)

private def updateStatus(iri: GroupIri, status: GroupStatus, user: User) =
for {
_ <- auth.ensureSystemAdminOrProjectAdminOfGroup(user, iri)
uuid <- Random.nextUUID
internal <- responder.deleteGroup(iri, uuid)
external <- format.toExternalADM(internal)
groupAndProject <- auth.ensureSystemAdminOrProjectAdminOfGroup(user, iri)
(group, _) = groupAndProject
updated <- knoraGroupService.updateGroupStatus(group, status)
internal <- groupService.toGroup(updated).map(GroupGetResponseADM.apply)
external <- format.toExternalADM(internal)
} yield external
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final case class GroupService(

private def toGroups(knoraGroups: Chunk[KnoraGroup]): Task[Chunk[Group]] = ZIO.foreach(knoraGroups)(toGroup)

private[service] def toGroup(knoraGroup: KnoraGroup): Task[Group] =
def toGroup(knoraGroup: KnoraGroup): Task[Group] =
for {
project <- knoraGroup.belongsToProject.map(projectService.findById).getOrElse(ZIO.none)
} yield Group(
Expand All @@ -60,9 +60,6 @@ final case class GroupService(

def updateGroup(groupToUpdate: Group, request: GroupUpdateRequest): Task[Group] =
knoraGroupService.updateGroup(toKnoraGroup(groupToUpdate), request).flatMap(toGroup)

def updateGroupStatus(groupToUpdate: Group, status: GroupStatus): Task[Group] =
knoraGroupService.updateGroupStatus(toKnoraGroup(groupToUpdate), status).flatMap(toGroup)
}

object GroupService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ import org.knora.webapi.slice.common.repo.rdf.Errors.RdfError
import org.knora.webapi.slice.common.repo.rdf.RdfResource
import org.knora.webapi.store.triplestore.api.TriplestoreService

final case class KnoraGroupRepoLive(triplestore: TriplestoreService, mapper: RdfEntityMapper[KnoraGroup])
extends AbstractEntityRepo[KnoraGroup, GroupIri](triplestore, mapper)
final case class KnoraGroupRepoLive(
private val triplestore: TriplestoreService,
private val mapper: RdfEntityMapper[KnoraGroup],
private val cache: EntityCache[GroupIri, KnoraGroup],
) extends CachingEntityRepo[KnoraGroup, GroupIri](triplestore, mapper, cache)
with KnoraGroupRepo {
override protected def resourceClass: ParsedIRI = ParsedIRI.create(KnoraAdmin.UserGroup)
override protected def namedGraphIri: Iri = Vocabulary.NamedGraphs.dataAdmin
Expand Down Expand Up @@ -91,5 +94,6 @@ object KnoraGroupRepoLive {
.andHas(hasSelfJoinEnabled, Rdf.literalOf(group.hasSelfJoinEnabled.value))
}

val layer = ZLayer.succeed(mapper) >>> ZLayer.derive[KnoraGroupRepoLive]
val layer = (ZLayer.succeed(mapper) >+> EntityCache.layer[GroupIri, KnoraGroup]("knoraGroup")) >>> ZLayer
.derive[KnoraGroupRepoLive]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import zio._

import dsp.errors.ForbiddenException
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraGroup
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.User
Expand All @@ -31,7 +32,7 @@ final case class AuthorizationRestService(
def ensureSystemAdminOrProjectAdminOfGroup(
user: User,
groupIri: GroupIri,
): IO[ForbiddenException, KnoraProject] =
): IO[ForbiddenException, (KnoraGroup, KnoraProject)] =
for {
group <- knoraGroupService
.findById(groupIri)
Expand All @@ -41,7 +42,7 @@ final case class AuthorizationRestService(
.succeed(group.belongsToProject)
.someOrFail(ForbiddenException(s"Group with IRI '${groupIri.value}' not found"))
project <- ensureSystemAdminOrProjectAdmin(user, projectIri)
} yield project
} yield (group, project)

def ensureSystemAdminOrProjectAdmin(
user: User,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ trait JwtService {
* @param token the JWT.
* @return a [[Boolean]].
*/
def validateToken(token: String): Task[Boolean]
def isTokenValid(token: String): Boolean

/**
* Extracts the encoded user IRI. This method also makes sure that the required headers and claims are present.
Expand Down Expand Up @@ -159,8 +159,7 @@ final case class JwtServiceLive(
* @param token the JWT.
* @return a [[Boolean]].
*/
override def validateToken(token: String): Task[Boolean] =
ZIO.succeed(!cache.contains(token) && decodeToken(token).isDefined)
override def isTokenValid(token: String): Boolean = !cache.contains(token) && decodeToken(token).isDefined

/**
* Extracts the encoded user IRI. This method also makes sure that the required headers and claims are present.
Expand Down

This file was deleted.

Loading

0 comments on commit 57a3a06

Please sign in to comment.