Skip to content

Commit

Permalink
chore: Add integration tests for project erasure (DEV-3681) (#3289)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Jun 18, 2024
1 parent d1fe63a commit bc07d66
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 43 deletions.
112 changes: 99 additions & 13 deletions integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@
*/

package org.knora.webapi
import zio.Chunk
import zio.NonEmptyChunk
import zio.ZIO
import zio.http.Status
import zio.test.Spec
import zio.test.TestAspect
import zio.test.assertTrue

import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger

import dsp.valueobjects.LanguageCode
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.messages.util.KnoraSystemInstances
import org.knora.webapi.messages.v2.responder.ontologymessages.CreateOntologyRequestV2
import org.knora.webapi.responders.v2.OntologyResponderV2
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest
import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest
import org.knora.webapi.slice.admin.domain.model.AdministrativePermissionPart
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.DefaultObjectAccessPermissionPart
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.Group
import org.knora.webapi.slice.admin.domain.model.Email
import org.knora.webapi.slice.admin.domain.model.FamilyName
import org.knora.webapi.slice.admin.domain.model.GivenName
Expand All @@ -29,15 +40,20 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname
import org.knora.webapi.slice.admin.domain.model.Password
import org.knora.webapi.slice.admin.domain.model.Permission
import org.knora.webapi.slice.admin.domain.model.Permission.Administrative.ProjectResourceCreateAll
import org.knora.webapi.slice.admin.domain.model.SystemAdmin
import org.knora.webapi.slice.admin.domain.model.UserIri
import org.knora.webapi.slice.admin.domain.model.UserStatus
import org.knora.webapi.slice.admin.domain.model.Username
import org.knora.webapi.slice.admin.domain.service.AdministrativePermissionService
import org.knora.webapi.slice.admin.domain.service.DefaultObjectAccessPermissionService
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.ProjectService
import org.knora.webapi.slice.resourceinfo.domain.InternalIri
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.store.triplestore.api.TriplestoreService
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Ask
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update
Expand All @@ -48,6 +64,8 @@ object ProjectEraseIT extends E2EZSpec {
private val projects = ZIO.serviceWithZIO[KnoraProjectService]
private val groups = ZIO.serviceWithZIO[KnoraGroupService]
private val db = ZIO.serviceWithZIO[TriplestoreService]
private val aps = ZIO.serviceWithZIO[AdministrativePermissionService]
private val doaps = ZIO.serviceWithZIO[DefaultObjectAccessPermissionService]

private def getUser(userIri: UserIri) = users(_.findById(userIri)).someOrFail(Exception(s"Must be present $userIri"))
private val shortcode = Shortcode.unsafeFrom("9999")
Expand Down Expand Up @@ -85,19 +103,25 @@ object ProjectEraseIT extends E2EZSpec {
),
)

private def createGroup(project: KnoraProject) = groups(
_.createGroup(
GroupCreateRequest(
None,
GroupName.unsafeFrom("group"),
GroupDescriptions.unsafeFrom(Seq(StringLiteralV2.unsafeFrom("group description", None))),
project.id,
GroupStatus.active,
GroupSelfJoin.enabled,
private val groupNr: AtomicInteger = new AtomicInteger()
private def createGroup(project: KnoraProject) = {
val nr = groupNr.getAndIncrement()
groups(
_.createGroup(
GroupCreateRequest(
None,
GroupName.unsafeFrom("group" + nr),
GroupDescriptions.unsafeFrom(
Seq(StringLiteralV2.unsafeFrom("group description: " + nr, None)),
),
project.id,
GroupStatus.active,
GroupSelfJoin.enabled,
),
project,
),
project,
),
)
)
}

private val createUserWithMemberships = for {
user <- createUser
Expand All @@ -108,6 +132,8 @@ object ProjectEraseIT extends E2EZSpec {
user <- users(_.addUserToProjectAsAdmin(user, project))
} yield (user, group)

private def doesGraphExist(graphName: InternalIri) = db(_.query(Ask(s"ASK { GRAPH <${graphName.value}> {} }")))

override def e2eSpec: Spec[ProjectEraseIT.env, Any] =
suiteAll(s"The erasing project endpoint ${AdminApiRestClient.projectsShortcodeErasePath}") {

Expand Down Expand Up @@ -153,7 +179,6 @@ object ProjectEraseIT extends E2EZSpec {
)
},
test("when called as root then it should delete the project graph") {
def doesGraphExist(graphName: InternalIri) = db(_.query(Ask(s"ASK { GRAPH <${graphName.value}> {} }")))
for {
// given
project <- getProject
Expand All @@ -178,6 +203,67 @@ object ProjectEraseIT extends E2EZSpec {
graphDeleted,
)
},
test("when called as root then it should delete the ontology graph") {
for {
project <- getProject
projectSmartIri <- ZIO.serviceWithZIO[IriConverter](_.asSmartIri(project.id.value))
req = CreateOntologyRequestV2(
"test",
projectSmartIri,
false,
"some label",
None,
UUID.randomUUID(),
KnoraSystemInstances.Users.SystemUser,
)
onto <- ZIO.serviceWithZIO[OntologyResponderV2](_.createOntology(req))
ontologyGraphName = onto.ontologies.head.ontologyIri.toInternalIri
ontologyGraphExists <- doesGraphExist(ontologyGraphName)

// when
erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode)

// then
graphDeleted <- doesGraphExist(ontologyGraphName).negate
} yield assertTrue(ontologyGraphExists, graphDeleted)
},
test("when called as root then it should delete administrative permissions") {
for {
project <- getProject
group <- createGroup(project)
perms = Chunk(
AdministrativePermissionPart.Simple
.from(ProjectResourceCreateAll)
.getOrElse(throw Exception("should not happen")),
)
ap <- aps(_.create(project, group, perms))
wasPresent <- aps(_.findByGroupAndProject(group.id, project.id)).map(_.nonEmpty)

// when
erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode)

// then
wasDeleted <- aps(_.findByGroupAndProject(group.id, project.id)).map(_.isEmpty)
} yield assertTrue(wasPresent, wasDeleted)
},
test("when called as root then it should delete the default object access permissions") {
for {
project <- getProject
group <- createGroup(project)
perms = Chunk(
DefaultObjectAccessPermissionPart(Permission.ObjectAccess.View, NonEmptyChunk(group.id)),
DefaultObjectAccessPermissionPart(Permission.ObjectAccess.Modify, NonEmptyChunk(group.id)),
)
doap <- doaps(_.create(project, Group(group.id), perms))
wasPresent <- doaps(_.findByProject(project.id)).map(_.nonEmpty)

// when
erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode)

// then
wasDeleted <- doaps(_.findByProject(project.id)).map(_.isEmpty)
} yield assertTrue(wasPresent, wasDeleted)
},
) @@ TestAspect.before(createProject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ object LayersTest {
CommonR0 & CommonR,
CommonR1,
](
OntologyResponderV2Live.layer,
OntologyResponderV2.layer,
StandoffResponderV2.layer,
ResourcesResponderV2.layer,
RepositoryUpdater.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ object LayersLive {
OntologyCacheLive.layer,
OntologyCacheHelpersLive.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
OntologyResponderV2.layer,
OntologyServiceLive.layer,
OntologyTriplestoreHelpersLive.layer,
PermissionUtilADMLive.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update
*
* The API v1 ontology responder, which is read-only, delegates most of its work to this responder.
*/
trait OntologyResponderV2

final case class OntologyResponderV2Live(
final case class OntologyResponderV2(
appConfig: AppConfig,
cardinalityHandler: CardinalityHandler,
cardinalityService: CardinalityService,
Expand All @@ -79,8 +77,7 @@ final case class OntologyResponderV2Live(
knoraProjectService: KnoraProjectService,
triplestoreService: TriplestoreService,
)(implicit val stringFormatter: StringFormatter)
extends OntologyResponderV2
with MessageHandler
extends MessageHandler
with LazyLogging {

override def isResponsibleFor(message: ResponderRequest): Boolean = message.isInstanceOf[OntologiesResponderRequestV2]
Expand Down Expand Up @@ -441,7 +438,7 @@ final case class OntologyResponderV2Live(
* @param createOntologyRequest the request message.
* @return a [[SuccessResponseV2]].
*/
private def createOntology(createOntologyRequest: CreateOntologyRequestV2): Task[ReadOntologyMetadataV2] = {
def createOntology(createOntologyRequest: CreateOntologyRequestV2): Task[ReadOntologyMetadataV2] = {
def makeTaskFuture(internalOntologyIri: SmartIri): Task[ReadOntologyMetadataV2] =
for {
// Make sure the ontology doesn't already exist.
Expand Down Expand Up @@ -522,7 +519,9 @@ final case class OntologyResponderV2Live(
// check if the requesting user is allowed to create an ontology
_ <-
ZIO.when(
!(requestingUser.permissions.isProjectAdmin(projectIri.toString) || requestingUser.permissions.isSystemAdmin),
!(requestingUser.permissions.isProjectAdmin(
projectIri.toString,
) || requestingUser.permissions.isSystemAdmin || requestingUser.isSystemUser),
) {
val msg =
s"A new ontology in the project ${createOntologyRequest.projectIri} can only be created by an admin of that project, or by a system admin."
Expand Down Expand Up @@ -2984,7 +2983,7 @@ final case class OntologyResponderV2Live(
}
}

object OntologyResponderV2Live {
object OntologyResponderV2 {
val layer: URLayer[
AppConfig & CardinalityHandler & CardinalityService & IriService & KnoraProjectService & MessageRelay &
OntologyCache & OntologyTriplestoreHelpers & OntologyCacheHelpers & OntologyRepo & StringFormatter &
Expand All @@ -3003,7 +3002,7 @@ object OntologyResponderV2Live {
or <- ZIO.service[OntologyRepo]
sf <- ZIO.service[StringFormatter]
ts <- ZIO.service[TriplestoreService]
responder = OntologyResponderV2Live(ac, ch, cs, is, oc, och, oth, or, kr, ts)(sf)
responder = OntologyResponderV2(ac, ch, cs, is, oc, och, oth, or, kr, ts)(sf)
_ <- ZIO.serviceWithZIO[MessageRelay](_.subscribe(responder))
} yield responder
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ object AdminDomainModule
TriplestoreService
,
AdministrativePermissionService &
DefaultObjectAccessPermissionService &
GroupService &
KnoraGroupService &
KnoraProjectService &
Expand All @@ -54,6 +55,7 @@ object AdminDomainModule
inline def layer: URLayer[self.Dependencies, self.Provided] =
ZLayer.makeSome[self.Dependencies, self.Provided](
AdministrativePermissionService.layer,
DefaultObjectAccessPermissionService.layer,
GroupService.layer,
KnoraGroupService.layer,
KnoraProjectService.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,51 @@
package org.knora.webapi.slice.admin.domain.model

import zio.Chunk
import zio.NonEmptyChunk
import zio.Task

import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.DefaultObjectAccessPermissionPart
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.repo.service.EntityWithId
import org.knora.webapi.slice.common.repo.service.CrudRepository
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

final case class DefaultObjectAccessPermission(
id: PermissionIri,
forProject: ProjectIri,
forWhat: ForWhat,
permission: Chunk[DefaultObjectAccessPermissionPart],
) extends EntityWithId[PermissionIri]

object DefaultObjectAccessPermission {
enum ForWhat {
case Group(iri: GroupIri)
case ResourceClass(iri: InternalIri)
case Property(iri: InternalIri)
case ResourceClassAndProperty(resourceClass: InternalIri, property: InternalIri)
}
object ForWhat {
def from(
group: Option[GroupIri],
resourceClass: Option[InternalIri],
property: Option[InternalIri],
): Either[String, ForWhat] =
(group, resourceClass, property) match {
case (None, Some(rc: InternalIri), Some(p: InternalIri)) => Right(ResourceClassAndProperty(rc, p))
case (None, None, Some(p: InternalIri)) => Right(Property(p))
case (None, Some(rc: InternalIri), None) => Right(ResourceClass(rc))
case (Some(g: GroupIri), None, None) => Right(Group(g))
case _ => Left(s"Invalid combination of group $group resourceClass $resourceClass and property $property.")
}
}

final case class DefaultObjectAccessPermissionPart(
permission: Permission.ObjectAccess,
groups: NonEmptyChunk[GroupIri],
)
}

trait DefaultObjectAccessPermissionRepo extends CrudRepository[DefaultObjectAccessPermission, PermissionIri] {

def findByProject(projectIri: ProjectIri): Task[Chunk[DefaultObjectAccessPermission]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,36 @@

package org.knora.webapi.slice.admin.domain.service

import zio.Chunk
import zio.Task
import zio.ZLayer

import org.knora.webapi.slice.admin.domain.model.AdministrativePermission
import org.knora.webapi.slice.admin.domain.model.AdministrativePermissionPart
import org.knora.webapi.slice.admin.domain.model.AdministrativePermissionRepo
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.PermissionIri

final case class AdministrativePermissionService(repo: AdministrativePermissionRepo) {
def findByGroupAndProject(groupIri: GroupIri, projectIri: ProjectIri): Task[Option[AdministrativePermission]] =
repo.findByGroupAndProject(groupIri, projectIri)

def create(
project: KnoraProject,
group: KnoraGroup,
permissions: Chunk[AdministrativePermissionPart],
): Task[AdministrativePermission] =
repo.save(
AdministrativePermission(
PermissionIri.makeNew(project.shortcode),
group.id,
project.id,
permissions,
),
)
}

object AdministrativePermissionService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.slice.admin.domain.service
import zio.Chunk
import zio.Task
import zio.ZLayer

import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.DefaultObjectAccessPermissionPart
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat
import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermissionRepo
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.PermissionIri

final case class DefaultObjectAccessPermissionService(
private val repo: DefaultObjectAccessPermissionRepo,
) {
def findByProject(projectIri: ProjectIri): Task[Chunk[DefaultObjectAccessPermission]] =
repo.findByProject(projectIri)

def create(
project: KnoraProject,
forWhat: ForWhat,
permission: Chunk[DefaultObjectAccessPermissionPart],
): Task[DefaultObjectAccessPermission] =
repo.save(DefaultObjectAccessPermission(PermissionIri.makeNew(project.shortcode), project.id, forWhat, permission))
}

object DefaultObjectAccessPermissionService {
val layer = ZLayer.derive[DefaultObjectAccessPermissionService]
}
Loading

0 comments on commit bc07d66

Please sign in to comment.