Skip to content

Commit

Permalink
refactor: Introduce OntologyService and refactor resource creation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
BalduinLandolt authored May 29, 2024
1 parent 211caa2 commit 21b82bf
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import org.knora.webapi.slice.ontology.api.service.RestCardinalityService
import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive
import org.knora.webapi.slice.ontology.domain.service.CardinalityService
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.domain.service.OntologyServiceLive
import org.knora.webapi.slice.ontology.repo.service.OntologyCache
import org.knora.webapi.slice.ontology.repo.service.OntologyCacheLive
import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive
Expand Down Expand Up @@ -168,6 +169,7 @@ object LayersTest {
OntologyHelpersLive.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
OntologyServiceLive.layer,
PermissionUtilADMLive.layer,
PermissionsResponder.layer,
PredicateObjectMapper.layer,
Expand Down
2 changes: 2 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import org.knora.webapi.slice.ontology.api.service.RestCardinalityService
import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive
import org.knora.webapi.slice.ontology.domain.service.CardinalityService
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.domain.service.OntologyServiceLive
import org.knora.webapi.slice.ontology.repo.service.OntologyCache
import org.knora.webapi.slice.ontology.repo.service.OntologyCacheLive
import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive
Expand Down Expand Up @@ -154,6 +155,7 @@ object LayersLive {
OntologyHelpersLive.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
OntologyServiceLive.layer,
PermissionUtilADMLive.layer,
PermissionsResponder.layer,
PredicateObjectMapper.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ object StringFormatter {
StringFormatter.initForTest()
StringFormatter.getGeneralInstance
}

def isKnoraOntologyIri(iri: SmartIri): Boolean =
iri.isKnoraApiV2DefinitionIri && OntologyConstants.InternalOntologyLabels.contains(iri.getOntologyName)
}

/**
Expand Down Expand Up @@ -985,16 +988,10 @@ class StringFormatter private (
}

override def getOntologyName: String =
iriInfo.ontologyName match {
case Some(name) => name
case None => throw DataConversionException(s"Expected a Knora ontology IRI: $iri")
}
iriInfo.ontologyName.getOrElse(throw DataConversionException(s"Expected a Knora ontology IRI: $iri"))

override def getEntityName: String =
iriInfo.entityName match {
case Some(name) => name
case None => throw DataConversionException(s"Expected a Knora entity IRI: $iri")
}
iriInfo.entityName.getOrElse(throw DataConversionException(s"Expected a Knora entity IRI: $iri"))

override def getOntologySchema: Option[OntologySchema] = iriInfo.ontologySchema

Expand Down Expand Up @@ -1626,16 +1623,6 @@ class StringFormatter private (
s"$mappingIri/elements/$knoraMappingElementUuid"
}

/**
* Creates an IRI used as a lock for the creation of mappings inside a given project.
* This method will always return the same IRI for the given project IRI.
*
* @param projectIri the IRI of the project the mapping will belong to.
* @return an IRI used as a lock for the creation of mappings inside a given project.
*/
def createMappingLockIriForProject(projectIri: IRI): IRI =
s"$projectIri/mappings"

/**
* Validates a custom value IRI, throwing [[BadRequestException]] if the IRI is not valid.
*
Expand All @@ -1660,18 +1647,10 @@ class StringFormatter private (
customValueIri
}

def isKnoraOntologyIri(iri: SmartIri): Boolean =
iri.isKnoraApiV2DefinitionIri && OntologyConstants.InternalOntologyLabels.contains(iri.getOntologyName)

def unescapeStringLiteralSeq(stringLiteralSeq: StringLiteralSequenceV2): StringLiteralSequenceV2 =
StringLiteralSequenceV2(
stringLiterals = stringLiteralSeq.stringLiterals.map(stringLiteral =>
StringLiteralV2.from(Iri.fromSparqlEncodedString(stringLiteral.value), stringLiteral.language),
),
)
def unescapeOptionalString(optionalString: Option[String]): Option[String] =
optionalString match {
case Some(s: String) => Some(Iri.fromSparqlEncodedString(s))
case None => None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.admin.domain.service.ProjectService
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.domain.service.OntologyService
import org.knora.webapi.store.iiif.errors.SipiException
import org.knora.webapi.store.triplestore.api.TriplestoreService
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct
Expand Down Expand Up @@ -89,6 +90,7 @@ final case class ResourcesResponderV2(
searchResponderV2: SearchResponderV2,
ontologyRepo: OntologyRepo,
permissionsResponder: PermissionsResponder,
ontologyService: OntologyService,
)(implicit val stringFormatter: StringFormatter)
extends MessageHandler
with LazyLogging
Expand All @@ -107,6 +109,7 @@ final case class ResourcesResponderV2(
this,
ontologyRepo,
permissionsResponder: PermissionsResponder,
ontologyService,
)

override def isResponsibleFor(message: ResponderRequest): Boolean =
Expand Down Expand Up @@ -2018,7 +2021,8 @@ object ResourcesResponderV2 {
or <- ZIO.service[OntologyRepo]
sf <- ZIO.service[StringFormatter]
pr <- ZIO.service[PermissionsResponder]
handler <- mr.subscribe(ResourcesResponderV2(config, iriS, mr, ts, cu, su, ru, pu, kps, sr, or, pr)(sf))
os <- ZIO.service[OntologyService]
handler <- mr.subscribe(ResourcesResponderV2(config, iriS, mr, ts, cu, su, ru, pu, kps, sr, or, pr, os)(sf))
} yield handler
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,7 @@ final case class StandoffResponderV2(
result <-
IriLocker.runWithIriLock(
apiRequestID,
stringFormatter
.createMappingLockIriForProject(
projectIri.toString,
), // use a special project specific IRI to lock the creation of mappings for the given project
s"${projectIri.toString}/mappings",
createMappingAndCheck(
xml = xml,
label = label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import org.knora.webapi.slice.ontology.domain.model.Cardinality.AtLeastOne
import org.knora.webapi.slice.ontology.domain.model.Cardinality.ExactlyOne
import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.domain.service.OntologyService
import org.knora.webapi.slice.ontology.domain.service.OntologyServiceLive
import org.knora.webapi.store.triplestore.api.TriplestoreService
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update
import org.knora.webapi.util.ZioHelper
Expand All @@ -59,6 +61,7 @@ final case class CreateResourceV2Handler(
getResources: GetResources,
ontologyRepo: OntologyRepo,
permissionsResponder: PermissionsResponder,
ontologyService: OntologyService,
)(implicit val stringFormatter: StringFormatter)
extends LazyLogging {

Expand Down Expand Up @@ -94,77 +97,74 @@ final case class CreateResourceV2Handler(
createResourceRequestV2: CreateResourceRequestV2,
): Task[ReadResourcesSequenceV2] =
for {
// Don't allow anonymous users to create resources.
_ <- ZIO.when(createResourceRequestV2.requestingUser.isAnonymousUser) {
ZIO.fail(ForbiddenException("Anonymous users aren't allowed to create resources"))
}

// Ensure that the project isn't the system project or the shared ontologies project.
_ <- ensureNotAnonymousUser(createResourceRequestV2.requestingUser)
_ <- ensureClassBelongsToProjectOntology(createResourceRequestV2)
projectIri = createResourceRequestV2.createResource.projectADM.id
_ <-
ZIO.when(
projectIri == KnoraProjectRepo.builtIn.SystemProject.id.value || projectIri == OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject,
)(ZIO.fail(BadRequestException(s"Resources cannot be created in project <$projectIri>")))

// Ensure that the resource class isn't from a non-shared ontology in another project.

resourceClassOntologyIri: SmartIri = createResourceRequestV2.createResource.resourceClassIri.getOntologyFromEntity
readOntologyMetadataV2 <- messageRelay
.ask[ReadOntologyMetadataV2](
OntologyMetadataGetByIriRequestV2(
Set(resourceClassOntologyIri),
createResourceRequestV2.requestingUser,
),
)
ontologyMetadata <- ZIO
.fromOption(readOntologyMetadataV2.ontologies.headOption)
.orElseFail(BadRequestException(s"Ontology $resourceClassOntologyIri not found"))
ontologyProjectIri <-
ZIO
.fromOption(ontologyMetadata.projectIri)
.mapBoth(
_ => InconsistentRepositoryDataException(s"Ontology $resourceClassOntologyIri has no project"),
_.toString(),
)

_ <-
ZIO.when(
projectIri != ontologyProjectIri && !(ontologyMetadata.ontologyIri.isKnoraBuiltInDefinitionIri || ontologyMetadata.ontologyIri.isKnoraSharedDefinitionIri),
) {
val msg =
s"Cannot create a resource in project <$projectIri> with resource class <${createResourceRequestV2.createResource.resourceClassIri}>, which is defined in a non-shared ontology in another project"
ZIO.fail(BadRequestException(msg))
}

// Check user's PermissionProfile (part of UserADM) to see if the user has the permission to
// create a new resource in the given project.

internalResourceClassIri: SmartIri = createResourceRequestV2.createResource.resourceClassIri
.toOntologySchema(InternalSchema)

_ <- ZIO.when(
!createResourceRequestV2.requestingUser.permissions
.hasPermissionFor(ResourceCreateOperation(internalResourceClassIri.toString), projectIri),
) {
val msg =
s"User ${createResourceRequestV2.requestingUser.username} does not have permission to create a resource of class <${createResourceRequestV2.createResource.resourceClassIri}> in project <$projectIri>"
ZIO.fail(ForbiddenException(msg))
}
_ <- ensureUserHasPermission(createResourceRequestV2, projectIri)

resourceIri <-
iriService.checkOrCreateEntityIri(
createResourceRequestV2.createResource.resourceIri,
stringFormatter.makeRandomResourceIri(createResourceRequestV2.createResource.projectADM.shortcode),
)

// Do the remaining pre-update checks and the update while holding an update lock on the resource to be created.
taskResult <- IriLocker.runWithIriLock(
createResourceRequestV2.apiRequestID,
resourceIri,
makeTask(createResourceRequestV2, resourceIri),
)
} yield taskResult

private def ensureNotAnonymousUser(user: User): Task[Unit] =
ZIO
.when(user.isAnonymousUser)(ZIO.fail(ForbiddenException("Anonymous users aren't allowed to create resources")))
.ignore

private def ensureClassBelongsToProjectOntology(createResourceRequestV2: CreateResourceRequestV2): Task[Unit] = for {
projectIri <- ZIO.succeed(createResourceRequestV2.createResource.projectADM.id)
isSystemOrSharedProject =
projectIri == KnoraProjectRepo.builtIn.SystemProject.id.value ||
projectIri == OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject
_ <- ZIO.when(isSystemOrSharedProject)(
ZIO.fail(BadRequestException(s"Resources cannot be created in project <$projectIri>")),
)

resourceClassOntologyIri =
createResourceRequestV2.createResource.resourceClassIri.getOntologyFromEntity.toInternalIri
resourceClassProjectIri <-
ontologyService
.getProjectIriForOntologyIri(resourceClassOntologyIri)
.someOrFail(BadRequestException(s"Ontology $resourceClassOntologyIri not found"))

_ <-
ZIO
.fail(
BadRequestException(
s"Cannot create a resource in project <$projectIri> with resource class <${createResourceRequestV2.createResource.resourceClassIri}>, which is defined in a non-shared ontology in another project",
),
)
.unless(
projectIri == resourceClassProjectIri || OntologyServiceLive.isBuiltInOrSharedOntology(
resourceClassOntologyIri,
),
)
} yield ()

private def ensureUserHasPermission(createResourceRequestV2: CreateResourceRequestV2, projectIri: String) = for {
internalResourceClassIri <-
ZIO.succeed(createResourceRequestV2.createResource.resourceClassIri.toOntologySchema(InternalSchema))
_ <-
ZIO
.fail(
ForbiddenException(
s"User ${createResourceRequestV2.requestingUser.username} does not have permission to create a resource of class <${createResourceRequestV2.createResource.resourceClassIri}> in project <$projectIri>",
),
)
.unless(
createResourceRequestV2.requestingUser.permissions
.hasPermissionFor(ResourceCreateOperation(internalResourceClassIri.toString), projectIri),
)
} yield ()

private def makeTask(
createResourceRequestV2: CreateResourceRequestV2,
resourceIri: IRI,
Expand Down
12 changes: 5 additions & 7 deletions webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,11 @@ object RouteUtilZ {
def decodeUuid(uuidStr: String): IO[BadRequestException, UUID] =
ZIO.attempt(UuidUtil.decode(uuidStr)).orElseFail(BadRequestException(s"Invalid value UUID: $uuidStr"))

def ensureExternalOntologyName(iri: SmartIri): ZIO[StringFormatter, BadRequestException, SmartIri] =
ZIO.serviceWithZIO[StringFormatter] { sf =>
if (sf.isKnoraOntologyIri(iri)) {
ZIO.fail(BadRequestException(s"Internal ontology <$iri> cannot be served"))
} else {
ZIO.succeed(iri)
}
def ensureExternalOntologyName(iri: SmartIri): IO[BadRequestException, SmartIri] =
if (StringFormatter.isKnoraOntologyIri(iri)) {
ZIO.fail(BadRequestException(s"Internal ontology <$iri> cannot be served"))
} else {
ZIO.succeed(iri)
}

def ensureIsKnoraOntologyIri(iri: SmartIri): IO[BadRequestException, SmartIri] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.ontology.domain.service

import zio.*

import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.slice.ontology.repo.service.OntologyCache
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

trait OntologyService {
def getProjectIriForOntologyIri(ontologyIri: InternalIri): Task[Option[String]]
}

final case class OntologyServiceLive(ontologyCache: OntologyCache) extends OntologyService {
def getProjectIriForOntologyIri(ontologyIri: InternalIri): Task[Option[String]] =
ontologyCache.getCacheData.map { cacheData =>
cacheData.ontologies.map { case (k, v) => k.toString() -> v }
.get(ontologyIri.value)
.flatMap(_.ontologyMetadata.projectIri.map(_.toString()))
}
}

object OntologyServiceLive {
def isBuiltInOntology(ontologyIri: InternalIri): Boolean =
OntologyConstants.BuiltInOntologyLabels.contains(ontologyIri.value.split("/").last)

def isSharedOntology(ontologyIri: InternalIri): Boolean =
ontologyIri.value.split("/")(4) == "shared"

def isBuiltInOrSharedOntology(ontologyIri: InternalIri): Boolean =
isBuiltInOntology(ontologyIri) || isSharedOntology(ontologyIri)

val layer = ZLayer.derive[OntologyServiceLive]
}

0 comments on commit 21b82bf

Please sign in to comment.