diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADMSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADMSpec.scala index f879e0f540..8955d36a49 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADMSpec.scala @@ -162,7 +162,7 @@ class PermissionsMessagesADMSpec extends CoreSpec { SharedTestDataADM.imagesUser01, ), ) - assertFailsWithA[BadRequestException](exit, s"Invalid group IRI $groupIri") + assertFailsWithA[BadRequestException](exit, s"Group IRI is invalid: $groupIri") } "return 'BadRequest' if the supplied custom permission IRI for DefaultObjectAccessPermissionCreateRequestADM is not valid" in { @@ -180,7 +180,7 @@ class PermissionsMessagesADMSpec extends CoreSpec { SharedTestDataADM.imagesUser01, ), ) - assertFailsWithA[BadRequestException](exit, s"Invalid permission IRI: $permissionIri.") + assertFailsWithA[BadRequestException](exit, s"Couldn't parse IRI: $permissionIri") } "return 'BadRequest' if the no permissions supplied for DefaultObjectAccessPermissionCreateRequestADM" in { @@ -317,7 +317,11 @@ class PermissionsMessagesADMSpec extends CoreSpec { SharedTestDataADM.rootUser, ), ) - assertFailsWithA[BadRequestException](exit, "Not allowed to supply groupIri and resourceClassIri together.") + assertFailsWithA[BadRequestException]( + exit, + "DOAP restrictions must be either for a group, a resource class, a property, " + + "or a combination of a resource class and a property. ", + ) } "return 'BadRequest' if the both group and property are supplied for DefaultObjectAccessPermissionCreateRequestADM" in { @@ -334,7 +338,11 @@ class PermissionsMessagesADMSpec extends CoreSpec { SharedTestDataADM.rootUser, ), ) - assertFailsWithA[BadRequestException](exit, "Not allowed to supply groupIri and propertyIri together.") + assertFailsWithA[BadRequestException]( + exit, + "DOAP restrictions must be either for a group, a resource class, a property, " + + "or a combination of a resource class and a property. ", + ) } "return 'BadRequest' if propertyIri supplied for DefaultObjectAccessPermissionCreateRequestADM is not valid" in { @@ -350,25 +358,9 @@ class PermissionsMessagesADMSpec extends CoreSpec { SharedTestDataADM.rootUser, ), ) - assertFailsWithA[BadRequestException](exit, s"Invalid property IRI: ${SharedTestDataADM.customValueIRI}") - } - - "return 'BadRequest' if resourceClassIri supplied for DefaultObjectAccessPermissionCreateRequestADM is not valid" in { - val exit = UnsafeZioRun.run( - PermissionRestService.createDefaultObjectAccessPermission( - CreateDefaultObjectAccessPermissionAPIRequestADM( - forProject = anythingProjectIri, - forResourceClass = Some(ANYTHING_THING_RESOURCE_CLASS_LocalHost), - hasPermissions = Set( - PermissionADM.from(Permission.ObjectAccess.ChangeRights, KnoraGroupRepo.builtIn.ProjectMember.id.value), - ), - ), - SharedTestDataADM.rootUser, - ), - ) assertFailsWithA[BadRequestException]( exit, - s"Invalid resource class IRI: $ANYTHING_THING_RESOURCE_CLASS_LocalHost", + " is not a Knora property IRI", ) } @@ -386,7 +378,8 @@ class PermissionsMessagesADMSpec extends CoreSpec { ) assertFailsWithA[BadRequestException]( exit, - "Either a group, a resource class, a property, or a combination of resource class and property must be given.", + "DOAP restrictions must be either for a group, a resource class, a property, " + + "or a combination of a resource class and a property. ", ) } } diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderSpec.scala index 71bdfada9d..2043a7cd65 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderSpec.scala @@ -195,7 +195,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { PermissionADM.from(Permission.ObjectAccess.RestrictedView, SharedTestDataADM.thingSearcherGroup.id), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -224,7 +223,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { .from(Permission.ObjectAccess.RestrictedView, KnoraGroupRepo.builtIn.UnknownUser.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -250,7 +248,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { hasPermissions = Set(PermissionADM.from(Permission.ObjectAccess.Modify, KnoraGroupRepo.builtIn.KnownUser.id.value)), ), - rootUser, UUID.randomUUID(), ), ), @@ -277,7 +274,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { PermissionADM.from(Permission.ObjectAccess.ChangeRights, KnoraGroupRepo.builtIn.Creator.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -305,7 +301,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { .from(Permission.ObjectAccess.ChangeRights, KnoraGroupRepo.builtIn.ProjectMember.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -332,7 +327,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { PermissionADM.from(Permission.ObjectAccess.Modify, KnoraGroupRepo.builtIn.ProjectMember.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -358,7 +352,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { PermissionADM.from(Permission.ObjectAccess.Modify, KnoraGroupRepo.builtIn.KnownUser.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -386,7 +379,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { PermissionADM.from(Permission.ObjectAccess.Modify, KnoraGroupRepo.builtIn.ProjectMember.id.value), ), ), - rootUser, UUID.randomUUID(), ), ), @@ -418,7 +410,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { forGroup = Some(KnoraGroupRepo.builtIn.UnknownUser.id.value), hasPermissions = hasPermissions, ), - rootUser, UUID.randomUUID(), ), ), @@ -455,7 +446,6 @@ class PermissionsResponderSpec extends CoreSpec with ImplicitSender { forGroup = Some(KnoraGroupRepo.builtIn.ProjectAdmin.id.value), hasPermissions = hasPermissions, ), - rootUser, UUID.randomUUID(), ), ), diff --git a/integration/src/test/scala/org/knora/webapi/util/ZioScalaTestUtil.scala b/integration/src/test/scala/org/knora/webapi/util/ZioScalaTestUtil.scala index aca4a2305b..d672faebf8 100644 --- a/integration/src/test/scala/org/knora/webapi/util/ZioScalaTestUtil.scala +++ b/integration/src/test/scala/org/knora/webapi/util/ZioScalaTestUtil.scala @@ -23,6 +23,16 @@ object ZioScalaTestUtil { err.squash shouldBe a[T] err.squash.getMessage shouldEqual expectedError } - case _ => Assertions.fail(s"Expected Exit. Failure with specific T.") + case _ => Assertions.fail(s"Expected Exit.Failure with specific T.") } + + def assertFailsWithA[T <: Throwable: ClassTag](actual: Exit[Throwable, ?], errorPredicate: Throwable => Boolean) = + actual match { + case Exit.Failure(err) => { + val errSquashed = err.squash + errSquashed shouldBe a[T] + errorPredicate(errSquashed) shouldBe true + } + case _ => Assertions.fail(s"Expected Exit.Failure with specific T.") + } } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index f9abce6010..5ab766cec2 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -93,6 +93,7 @@ object LayersLive { AuthenticationApiModule.Provided & CardinalityHandler & ConstructResponseUtilV2 & + DefaultObjectAccessPermissionService & GroupRestService & HttpServer & IIIFRequestMessageHandler & diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponder.scala index 9bca490f05..e9b7e8adf4 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponder.scala @@ -26,16 +26,29 @@ import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService import org.knora.webapi.slice.admin.AdminConstants +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.Group +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.Property +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.ResourceClass +import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.ResourceClassAndProperty import org.knora.webapi.slice.admin.domain.model.GroupIri +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.Permission import org.knora.webapi.slice.admin.domain.model.PermissionIri import org.knora.webapi.slice.admin.domain.model.User 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.GroupService import org.knora.webapi.slice.admin.domain.service.KnoraGroupRepo.* import org.knora.webapi.slice.admin.domain.service.KnoraProjectService +import org.knora.webapi.slice.admin.repo.service.DefaultObjectAccessPermissionRepoLive +import org.knora.webapi.slice.common.KnoraIris.PropertyIri +import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri import org.knora.webapi.slice.common.api.AuthorizationRestService +import org.knora.webapi.slice.ontology.domain.service.OntologyRepo +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.Construct @@ -49,6 +62,9 @@ final case class PermissionsResponder( private val triplestore: TriplestoreService, private val auth: AuthorizationRestService, private val administrativePermissionService: AdministrativePermissionService, + private val iriConverter: IriConverter, + private val ontologyRepo: OntologyRepo, + private val doapService: DefaultObjectAccessPermissionService, )(implicit val stringFormatter: StringFormatter) extends LazyLogging { @@ -534,151 +550,66 @@ final case class PermissionsResponder( } yield DefaultObjectAccessPermissionsStringResponseADM(resultStr) } - private def validate(req: CreateDefaultObjectAccessPermissionAPIRequestADM) = ZIO.attempt { - val sf: StringFormatter = StringFormatter.getInstanceForConstantOntologies + private def validate( + req: CreateDefaultObjectAccessPermissionAPIRequestADM, + ): IO[String, DefaultObjectAccessPermission] = + for { + projectIri <- ZIO.fromEither(ProjectIri.from(req.forProject)) + project <- knoraProjectService.findById(projectIri).orDie.someOrFail("Project not found") + permissionIri <- + ZIO + .foreach(req.id)(iriConverter.asSmartIri) + .flatMap(iriService.checkOrCreateEntityIri(_, PermissionIri.makeNew(project.shortcode).value)) + .mapBoth(_.getMessage, PermissionIri.unsafeFrom) - req.id.foreach(iri => PermissionIri.from(iri).fold(msg => throw BadRequestException(msg), _ => ())) + groupIri <- ZIO.fromEither(req.forGroup.fold(Right(None))(GroupIri.from(_).map(Some(_)))) + _ <- ZIO.foreachDiscard(groupIri)(groupService.findById(_).orDie.someOrFail("Group not found")) - ProjectIri.from(req.forProject).getOrElse(throw BadRequestException(s"Invalid project IRI ${req.forProject}")) + resourceClassIri <- + ZIO.foreach(req.forResourceClass)( + iriConverter.asSmartIri(_).mapError(_.getMessage).flatMap(s => ZIO.fromEither(ResourceClassIri.from(s))), + ) - (req.forGroup, req.forResourceClass, req.forProperty) match { - case (None, None, None) => - throw BadRequestException( - "Either a group, a resource class, a property, or a combination of resource class and property must be given.", + propertyIri <- + ZIO.foreach(req.forProperty)( + iriConverter.asSmartIri(_).mapError(_.getMessage).flatMap(s => ZIO.fromEither(PropertyIri.from(s))), ) - case (Some(_), Some(_), _) => - throw BadRequestException("Not allowed to supply groupIri and resourceClassIri together.") - case (Some(_), _, Some(_)) => - throw BadRequestException("Not allowed to supply groupIri and propertyIri together.") - case (Some(groupIri), None, None) => - GroupIri.from(groupIri).getOrElse(throw BadRequestException(s"Invalid group IRI $groupIri")) - case (None, resourceClassIriMaybe, propertyIriMaybe) => - resourceClassIriMaybe.foreach { resourceClassIri => - if (!sf.toSmartIri(resourceClassIri).isKnoraEntityIri) { - throw BadRequestException(s"Invalid resource class IRI: $resourceClassIri") - } - } - propertyIriMaybe.foreach { propertyIri => - if (!sf.toSmartIri(propertyIri).isKnoraEntityIri) { - throw BadRequestException(s"Invalid property IRI: $propertyIri") - } - } - case _ => () - } + _ <- ZIO.foreachDiscard(propertyIri)( + ontologyRepo.findProperty(_).orDie.someOrFail(s"Property $propertyIri not found"), + ) - if (req.hasPermissions.isEmpty) throw BadRequestException("Permissions needs to be supplied.") - } + forWhat <- ZIO.fromEither(ForWhat.fromIris(groupIri, resourceClassIri, propertyIri)) + _ <- ZIO.fail("Permissions needs to be supplied.").when(req.hasPermissions.isEmpty) + doap <- ZIO.fromEither(DefaultObjectAccessPermission.from(permissionIri, projectIri, forWhat, req.hasPermissions)) + } yield doap def createDefaultObjectAccessPermission( createRequest: CreateDefaultObjectAccessPermissionAPIRequestADM, - user: User, apiRequestID: UUID, ): Task[DefaultObjectAccessPermissionCreateResponseADM] = { - - /** - * The actual change project task run with an IRI lock. - */ val createPermissionTask = for { - _ <- validate(createRequest) - projectIri <- ZIO.fromEither(ProjectIri.from(createRequest.forProject)).mapError(BadRequestException.apply) - project <- knoraProjectService - .findById(projectIri) - .someOrFail(NotFoundException(s"Project ${projectIri.value} not found")) - _ <- auth.ensureSystemAdminOrProjectAdmin(user, project) - checkResult <- defaultObjectAccessPermissionGetADM( - projectIri, - createRequest.forGroup, - createRequest.forResourceClass, - createRequest.forProperty, - ) - - _ = checkResult match { - case Some(doap: DefaultObjectAccessPermissionADM) => - val errorMessage = if (doap.forGroup.nonEmpty) { - s"and group: '${doap.forGroup.get}' " - } else { - val resourceClassExists = if (doap.forResourceClass.nonEmpty) { - s"and resourceClass: '${doap.forResourceClass.get}' " - } else "" - val propExists = if (doap.forProperty.nonEmpty) { - s"and property: '${doap.forProperty.get}' " - } else "" - resourceClassExists + propExists - } - throw DuplicateValueException( - s"A default object access permission for project: '${createRequest.forProject}' " + - errorMessage + "combination already exists. " + - s"This permission currently has the scope '${PermissionUtilADM - .formatPermissionADMs(doap.hasPermissions, PermissionType.OAP)}'. " + - s"Use its IRI ${doap.iri} to modify it, if necessary.", - ) - case None => () - } - - customPermissionIri: Option[SmartIri] = createRequest.id.map(iri => stringFormatter.toSmartIri(iri)) - newPermissionIri <- iriService.checkOrCreateEntityIri( - customPermissionIri, - PermissionIri.makeNew(project.shortcode).value, - ) - // verify group, if any given. - // Is a group given that is not a built-in one? - maybeGroupIri <- - if (createRequest.forGroup.exists(!builtIn.all.map(_.id.value).contains(_))) { - // Yes. Check if it is a known group. - for { - maybeIri <- ZIO - .fromOption(createRequest.forGroup) - .orElseFail(NotFoundException("Group IRI not found.")) - groupIri <- ZIO.fromEither(GroupIri.from(maybeIri)).mapError(ValidationException(_)) - group <- - groupService - .findById(groupIri) - .someOrFail(NotFoundException(s"Group '${createRequest.forGroup}' not found. Aborting request.")) - } yield Some(group.id) - } else { - // No, return given group as it is. That means: - // If given group is a built-in one, no verification is necessary, return it as it is. - // In case no group IRI is given, returns None. - ZIO.succeed(createRequest.forGroup) - } - - // Create the default object access permission. - permissions <- verifyHasPermissionsDOAP(createRequest.hasPermissions) - createNewDefaultObjectAccessPermissionSparqlString = sparql.admin.txt.createNewDefaultObjectAccessPermission( - AdminConstants.permissionsDataNamedGraph.value, - permissionIri = newPermissionIri, - permissionClassIri = - OntologyConstants.KnoraAdmin.DefaultObjectAccessPermission, - projectIri = project.id.value, - maybeGroupIri = maybeGroupIri, - maybeResourceClassIri = createRequest.forResourceClass, - maybePropertyIri = createRequest.forProperty, - permissions = PermissionUtilADM.formatPermissionADMs( - permissions, - PermissionType.OAP, - ), - ) - _ <- triplestore.query(Update(createNewDefaultObjectAccessPermissionSparqlString)) - - // try to retrieve the newly created permission - maybePermission <- defaultObjectAccessPermissionGetADM( - projectIri, - createRequest.forGroup, - createRequest.forResourceClass, - createRequest.forProperty, - ) - - newDefaultObjectAcessPermission: DefaultObjectAccessPermissionADM = - maybePermission.getOrElse( - throw BadRequestException( - "Requested default object access permission could not be created, report this as a possible bug.", - ), - ) - - } yield DefaultObjectAccessPermissionCreateResponseADM(defaultObjectAccessPermission = - newDefaultObjectAcessPermission, - ) + doap <- validate(createRequest).mapError(BadRequestException(_)) + _ <- doapService.findByProjectAndForWhat(doap.forProject, doap.forWhat).flatMap { + case Some(existing: DefaultObjectAccessPermission) => + val msg = existing.forWhat match + case Group(g) => s"and group: '${g.value}' " + case ResourceClass(rc) => s"and resourceClass: '${rc.value}' " + case Property(prop) => s"and property: '${prop.value}' " + case ResourceClassAndProperty(rc, prop) => + s"and resourceClass: '${rc.value}' and property: '${prop.value}' " + ZIO.fail( + DuplicateValueException( + s"A default object access permission for project: '${doap.forProject.value}' " + + msg + "combination already exists. " + + s"This permission currently has the scope '${DefaultObjectAccessPermissionRepoLive.toStringLiteral(existing.permission)}'. " + + s"Use its IRI ${existing.id.value} to modify it, if necessary.", + ), + ) + case None => ZIO.unit + } + _ <- doapService.save(doap) + } yield DefaultObjectAccessPermissionCreateResponseADM(doapService.asDefaultObjectAccessPermissionADM(doap)) IriLocker.runWithIriLock(apiRequestID, PERMISSIONS_GLOBAL_LOCK_IRI, createPermissionTask) } @@ -1298,7 +1229,6 @@ final case class PermissionsResponder( PermissionADM.from(Permission.ObjectAccess.Delete, builtIn.ProjectMember.id.value), ), ), - KnoraSystemInstances.Users.SystemUser, UUID.randomUUID(), ) @@ -1312,7 +1242,6 @@ final case class PermissionsResponder( PermissionADM.from(Permission.ObjectAccess.Delete, builtIn.ProjectMember.id.value), ), ), - KnoraSystemInstances.Users.SystemUser, UUID.randomUUID(), ) } yield () diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionRestService.scala index fe22aa1f3b..c08c31cd16 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionRestService.scala @@ -101,7 +101,7 @@ final case class PermissionRestService( for { _ <- ensureProjectIriStrExistsAndUserHasAccess(request.forProject, user) uuid <- Random.nextUUID - result <- responder.createDefaultObjectAccessPermission(request, user, uuid) + result <- responder.createDefaultObjectAccessPermission(request, uuid) ext <- format.toExternal(result) } yield ext diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/DefaultObjectAccessPermissionRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/DefaultObjectAccessPermissionRepo.scala index 3dcbbec2bd..bb9e88c260 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/DefaultObjectAccessPermissionRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/DefaultObjectAccessPermissionRepo.scala @@ -9,10 +9,13 @@ import zio.Chunk import zio.NonEmptyChunk import zio.Task +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM 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.KnoraIris.PropertyIri +import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri import org.knora.webapi.slice.common.repo.service.CrudRepository import org.knora.webapi.slice.resourceinfo.domain.InternalIri @@ -29,8 +32,31 @@ object DefaultObjectAccessPermission { case ResourceClass(iri: InternalIri) case Property(iri: InternalIri) case ResourceClassAndProperty(resourceClass: InternalIri, property: InternalIri) + + def groupOption: Option[GroupIri] = this match { + case Group(iri) => Some(iri) + case _ => None + } + def resourceClassOption: Option[InternalIri] = this match { + case ResourceClass(iri) => Some(iri) + case ResourceClassAndProperty(iri, _) => Some(iri) + case _ => None + } + def propertyOption: Option[InternalIri] = this match { + case Property(iri) => Some(iri) + case ResourceClassAndProperty(_, iri) => Some(iri) + case _ => None + } } object ForWhat { + + def fromIris( + group: Option[GroupIri], + resourceClass: Option[ResourceClassIri], + property: Option[PropertyIri], + ): Either[String, ForWhat] = + from(group, resourceClass.map(_.toInternal), property.map(_.toInternal)) + def from( group: Option[GroupIri], resourceClass: Option[InternalIri], @@ -41,7 +67,11 @@ object DefaultObjectAccessPermission { 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.") + case _ => + Left( + s"DOAP restrictions must be either for a group, a resource class, a property, " + + s"or a combination of a resource class and a property. ", + ) } } @@ -49,6 +79,26 @@ object DefaultObjectAccessPermission { permission: Permission.ObjectAccess, groups: NonEmptyChunk[GroupIri], ) + object DefaultObjectAccessPermissionPart { + def from(adm: PermissionADM): Either[String, DefaultObjectAccessPermissionPart] = + for { + group <- adm.additionalInformation.toRight("No object access code present").flatMap(GroupIri.from) + perm = adm.permissionCode.flatMap(Permission.ObjectAccess.from).getOrElse(Permission.ObjectAccess.Delete) + } yield DefaultObjectAccessPermissionPart(perm, NonEmptyChunk(group)) + } + + def from( + id: PermissionIri, + forProject: ProjectIri, + forWhat: ForWhat, + perms: Set[PermissionADM], + ): Either[String, DefaultObjectAccessPermission] = + perms + .map(DefaultObjectAccessPermissionPart.from) + .map(_.map(Chunk(_))) + .fold(Right(Chunk.empty))((a, b) => a.flatMap(aa => b.map(bb => aa ++ bb))) + .flatMap(NonEmptyChunk.fromChunk(_).toRight("No permissions found")) + .map(DefaultObjectAccessPermission(id, forProject, forWhat, _)) } trait DefaultObjectAccessPermissionRepo extends CrudRepository[DefaultObjectAccessPermission, PermissionIri] { @@ -56,4 +106,6 @@ trait DefaultObjectAccessPermissionRepo extends CrudRepository[DefaultObjectAcce def findByProject(projectIri: ProjectIri): Task[Chunk[DefaultObjectAccessPermission]] final def findByProject(project: KnoraProject): Task[Chunk[DefaultObjectAccessPermission]] = findByProject(project.id) + + def findByProjectAndForWhat(projectIri: ProjectIri, forWhat: ForWhat): Task[Option[DefaultObjectAccessPermission]] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DefaultObjectAccessPermissionService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DefaultObjectAccessPermissionService.scala index 70e5d97062..74cdda8c9d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DefaultObjectAccessPermissionService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DefaultObjectAccessPermissionService.scala @@ -8,6 +8,8 @@ import zio.Chunk import zio.Task import zio.ZLayer +import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionADM +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM 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 @@ -28,6 +30,33 @@ final case class DefaultObjectAccessPermissionService( permission: Chunk[DefaultObjectAccessPermissionPart], ): Task[DefaultObjectAccessPermission] = repo.save(DefaultObjectAccessPermission(PermissionIri.makeNew(project.shortcode), project.id, forWhat, permission)) + + def save(permission: DefaultObjectAccessPermission): Task[DefaultObjectAccessPermission] = + repo.save(permission) + + def findByProjectAndForWhat(projectIri: ProjectIri, forWhat: ForWhat): Task[Option[DefaultObjectAccessPermission]] = + repo.findByProjectAndForWhat(projectIri, forWhat) + + def asDefaultObjectAccessPermissionADM(doap: DefaultObjectAccessPermission): DefaultObjectAccessPermissionADM = + DefaultObjectAccessPermissionADM( + doap.id.value, + doap.forProject.value, + doap.forWhat.groupOption.map(_.value), + doap.forWhat.resourceClassOption.map(_.value), + doap.forWhat.propertyOption.map(_.value), + asPermissionADM(doap.permission).toSet, + ) + + def asPermissionADM(parts: Chunk[DefaultObjectAccessPermissionPart]): Chunk[PermissionADM] = + parts.flatMap { part => + part.groups.map(group => + PermissionADM( + part.permission.token, + Some(group.value), + Some(part.permission.code), + ), + ) + } } object DefaultObjectAccessPermissionService { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLive.scala index f2cbc42e0f..9899751beb 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLive.scala @@ -24,9 +24,7 @@ import org.knora.webapi.slice.admin.AdminConstants.permissionsDataNamedGraph 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.DefaultObjectAccessPermission.ForWhat.Group -import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.ResourceClass -import org.knora.webapi.slice.admin.domain.model.DefaultObjectAccessPermission.ForWhat.ResourceClassAndProperty +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.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri @@ -57,13 +55,26 @@ final case class DefaultObjectAccessPermissionRepoLive( override def findByProject(projectIri: ProjectIri): Task[Chunk[DefaultObjectAccessPermission]] = findAllByTriplePattern(_.has(Vocabulary.KnoraAdmin.forProject, Rdf.iri(projectIri.value))) + + def findByProjectAndForWhat(projectIri: ProjectIri, forWhat: ForWhat): Task[Option[DefaultObjectAccessPermission]] = + findOneByTriplePattern(p => + val pattern = p.has(Vocabulary.KnoraAdmin.forProject, Rdf.iri(projectIri.value)) + forWhat match { + case Group(g) => pattern.andHas(Vocabulary.KnoraAdmin.forGroup, Rdf.iri(g.value)) + case ResourceClass(rc) => pattern.andHas(Vocabulary.KnoraAdmin.forResourceClass, Rdf.iri(rc.value)) + case Property(prop) => pattern.andHas(Vocabulary.KnoraAdmin.forProperty, Rdf.iri(prop.value)) + case ResourceClassAndProperty(rc, prop) => + pattern + .andHas(Vocabulary.KnoraAdmin.forResourceClass, Rdf.iri(rc.value)) + .andHas(Vocabulary.KnoraAdmin.forProperty, Rdf.iri(prop.value)) + }, + ) } object DefaultObjectAccessPermissionRepoLive { + private val permissionsDelimiter = '|' private val mapper = new RdfEntityMapper[DefaultObjectAccessPermission] { - private val permissionsDelimiter = '|' - override def toEntity(resource: RdfResource): IO[RdfError, DefaultObjectAccessPermission] = for { id <- resource.iri.flatMap { iri => ZIO.fromEither(PermissionIri.from(iri.value).left.map(ConversionError.apply)) @@ -130,13 +141,13 @@ object DefaultObjectAccessPermissionRepoLive { .andHas(Vocabulary.KnoraAdmin.forProperty, Rdf.iri(p.value)) } } + } - private def toStringLiteral(permissions: Chunk[DefaultObjectAccessPermissionPart]): String = { - def withOutPrefixExpansion(str: String) = str.replace(KnoraAdminPrefixExpansion, KnoraAdminPrefix) - permissions - .map(p => s"${p.permission.token} ${p.groups.map(_.value).map(withOutPrefixExpansion).mkString(",")}") - .mkString(permissionsDelimiter.toString) - } + def toStringLiteral(permissions: Chunk[DefaultObjectAccessPermissionPart]): String = { + def withOutPrefixExpansion(str: String) = str.replace(KnoraAdminPrefixExpansion, KnoraAdminPrefix) + permissions + .map(p => s"${p.permission.token} ${p.groups.map(_.value).map(withOutPrefixExpansion).mkString(",")}") + .mkString(permissionsDelimiter.toString) } val layer = ZLayer.succeed(mapper) >>> ZLayer.derive[DefaultObjectAccessPermissionRepoLive] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraApiCreateValueModel.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraApiCreateValueModel.scala deleted file mode 100644 index cde30bb677..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraApiCreateValueModel.scala +++ /dev/null @@ -1,173 +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.slice.common - -import org.apache.jena.rdf.model.* -import org.apache.jena.vocabulary.RDF -import zio.* - -import java.time.Instant -import java.util.UUID -import scala.jdk.CollectionConverters.* -import scala.language.implicitConversions - -import dsp.valueobjects.UuidUtil -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.* -import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.ValueHasUUID -import org.knora.webapi.messages.OntologyConstants.Xsd -import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.ValuesValidator -import org.knora.webapi.messages.v2.responder.valuemessages.* -import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo -import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode -import org.knora.webapi.slice.common.KnoraIris.* -import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri as KResourceClassIri -import org.knora.webapi.slice.common.KnoraIris.ResourceIri as KResourceIri -import org.knora.webapi.slice.common.jena.JenaConversions.given -import org.knora.webapi.slice.common.jena.ModelOps -import org.knora.webapi.slice.common.jena.ModelOps.* -import org.knora.webapi.slice.common.jena.ResourceOps.* -import org.knora.webapi.slice.common.jena.StatementOps.* -import org.knora.webapi.slice.resourceinfo.domain.IriConverter - -final case class KnoraApiCreateValueModel( - resourceIri: ResourceIri, - resourceClassIri: KResourceClassIri, - valuePropertyIri: PropertyIri, - valueType: SmartIri, - valueIri: Option[ValueIri], - valueUuid: Option[UUID], - valueCreationDate: Option[Instant], - valuePermissions: Option[String], - valueFileValueFilename: Option[String], - private val valueResource: Resource, - private val converter: IriConverter, -) { - lazy val shortcode: Shortcode = resourceIri.shortcode - - def getValueContent(fileInfo: Option[FileInfo] = None): ZIO[MessageRelay, String, ValueContentV2] = - def withFileInfo[T](f: FileInfo => Either[String, T]): IO[String, T] = - fileInfo match - case None => ZIO.fail("FileInfo is missing") - case Some(info) => ZIO.fromEither(f(info)) - valueType.toString match - case AudioFileValue => withFileInfo(AudioFileValueContentV2.from(valueResource, _)) - case ArchiveFileValue => withFileInfo(ArchiveFileValueContentV2.from(valueResource, _)) - case BooleanValue => ZIO.fromEither(BooleanValueContentV2.from(valueResource)) - case ColorValue => ZIO.fromEither(ColorValueContentV2.from(valueResource)) - case DateValue => ZIO.fromEither(DateValueContentV2.from(valueResource)) - case DecimalValue => ZIO.fromEither(DecimalValueContentV2.from(valueResource)) - case DocumentFileValue => withFileInfo(DocumentFileValueContentV2.from(valueResource, _)) - case GeomValue => ZIO.fromEither(GeomValueContentV2.from(valueResource)) - case GeonameValue => ZIO.fromEither(GeonameValueContentV2.from(valueResource)) - case IntValue => ZIO.fromEither(IntegerValueContentV2.from(valueResource)) - case IntervalValue => ZIO.fromEither(IntervalValueContentV2.from(valueResource)) - case ListValue => HierarchicalListValueContentV2.from(valueResource, converter) - case LinkValue => LinkValueContentV2.from(valueResource, converter) - case MovingImageFileValue => withFileInfo(MovingImageFileValueContentV2.from(valueResource, _)) - case StillImageExternalFileValue => ZIO.fromEither(StillImageExternalFileValueContentV2.from(valueResource)) - case StillImageFileValue => withFileInfo(StillImageFileValueContentV2.from(valueResource, _)) - case TextValue => TextValueContentV2.from(valueResource) - case TextFileValue => withFileInfo(TextFileValueContentV2.from(valueResource, _)) - case TimeValue => ZIO.fromEither(TimeValueContentV2.from(valueResource)) - case UriValue => ZIO.fromEither(UriValueContentV2.from(valueResource)) - case _ => ZIO.fail(s"Unsupported value type: $valueType") -} - -object KnoraApiCreateValueModel { self => - - // available for ease of use in tests - def fromJsonLd(str: String): ZIO[Scope & IriConverter, String, KnoraApiCreateValueModel] = - ZIO.service[IriConverter].flatMap(self.fromJsonLd(str, _)) - - def fromJsonLd(str: String, converter: IriConverter): ZIO[Scope & IriConverter, String, KnoraApiCreateValueModel] = - for { - model <- ModelOps.fromJsonLd(str) - resourceAndIri <- resourceAndIri(model, converter) - (resource, resourceIri) = resourceAndIri - resourceClassIri <- resourceClassIri(resource, converter) - valueStatement <- valueStatement(resource) - propertyIri <- valuePropertyIri(converter, valueStatement) - valueType <- valueType(valueStatement, converter) - valueResource = valueStatement.getObject.asResource() - valueIri <- valueIri(valueResource, converter) - valueUuid <- ZIO.fromEither(valueHasUuid(valueResource)) - valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource)) - valuePermissions <- ZIO.fromEither(valuePermissions(valueResource)) - valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource)) - } yield KnoraApiCreateValueModel( - resourceIri, - resourceClassIri, - propertyIri, - valueType, - valueIri, - valueUuid, - valueCreationDate, - valuePermissions, - valueFileValueFilename, - valueResource, - converter, - ) - - private def resourceAndIri(model: Model, convert: IriConverter): IO[String, (Resource, ResourceIri)] = - ZIO.fromEither(model.singleRootResource).flatMap { (r: Resource) => - convert - .asSmartIri(r.uri.getOrElse("")) - .mapError(_.getMessage) - .flatMap(iri => ZIO.fromEither(KResourceIri.from(iri))) - .map((r, _)) - } - - private def valueStatement(rootResource: Resource): IO[String, Statement] = ZIO - .succeed(rootResource.listProperties().asScala.filter(_.getPredicate != RDF.`type`).toList) - .filterOrFail(_.nonEmpty)("No value property found in root resource") - .filterOrFail(_.size == 1)("Multiple value properties found in root resource") - .map(_.head) - - private def valuePropertyIri(converter: IriConverter, valueStatement: Statement) = - converter - .asSmartIri(valueStatement.predicateUri) - .mapError(_.getMessage) - .flatMap(iri => ZIO.fromEither(PropertyIri.from(iri))) - - private def valueType(stmt: Statement, converter: IriConverter) = ZIO - .fromEither(stmt.objectAsResource().flatMap(_.rdfsType.toRight("No rdf:type found for value."))) - .orElseFail(s"No value type found for value.") - .flatMap(converter.asSmartIri(_).mapError(_.getMessage)) - - private def valueIri(valueResource: Resource, converter: IriConverter): IO[String, Option[ValueIri]] = ZIO - .fromOption(valueResource.uri) - .flatMap(converter.asSmartIri(_).mapError(_.getMessage).asSomeError) - .flatMap(iri => ZIO.fromEither(ValueIri.from(iri)).asSomeError) - .unsome - - private def valueHasUuid(valueResource: Resource): Either[String, Option[UUID]] = - valueResource.objectStringOption(ValueHasUUID).flatMap { - case Some(str) => - UuidUtil.base64Decode(str).map(Some(_)).toEither.left.map(e => s"Invalid UUID '$str': ${e.getMessage}") - case None => Right(None) - } - - private def valueCreationDate(valueResource: Resource): Either[String, Option[Instant]] = - valueResource.objectDataTypeOption(ValueCreationDate, Xsd.DateTimeStamp).flatMap { - case Some(str) => ValuesValidator.parseXsdDateTimeStamp(str).map(Some(_)) - case None => Right(None) - } - - private def valuePermissions(valueResource: Resource): Either[String, Option[String]] = - valueResource.objectStringOption(HasPermissions) - - private def valueFileValueFilename(valueResource: Resource): Either[String, Option[String]] = - valueResource.objectStringOption(FileValueHasFilename) - - private def resourceClassIri(rootResource: Resource, convert: IriConverter): IO[String, KResourceClassIri] = ZIO - .fromOption(rootResource.rdfsType) - .orElseFail("No root resource class IRI found") - .flatMap(convert.asSmartIri(_).mapError(_.getMessage)) - .flatMap(iri => ZIO.fromEither(KResourceClassIri.from(iri))) -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala index a2b0d2bec2..433b579033 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala @@ -4,10 +4,9 @@ */ package org.knora.webapi.slice.common + import eu.timepit.refined.types.string.NonEmptyString -import org.knora.webapi.ApiV2Complex -import org.knora.webapi.ApiV2Simple import org.knora.webapi.OntologySchema import org.knora.webapi.messages.SmartIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode @@ -17,26 +16,36 @@ object KnoraIris { opaque type ResourceId = NonEmptyString opaque type ValueId = NonEmptyString + opaque type EntityName = NonEmptyString trait KnoraIri { self => def smartIri: SmartIri override def toString: String = self.smartIri.toString def toInternal: InternalIri = self.smartIri.toInternalIri - def toApiV2Complex: SmartIri = self.toOntologySchema(ApiV2Complex) - def toApiV2Simple: SmartIri = self.toOntologySchema(ApiV2Simple) def toOntologySchema(s: OntologySchema): SmartIri = self.smartIri.toOntologySchema(s) } + // PropertyIri and ResourceClassIri currently have the same constraint + // i.e. they only have to be a KnoraEntityIri (SmartIri.isKnoraEntityIri) + // but they are kept separate for clarity of method signatures + + // Both Iris have an internal and external representation, thus we provide + // functions which create these from a SmartIri. `from` accepts any SmartIri. `fromApiV2Complex` only accepts + // SmartIris that are part of the API v2 complex schema. + final case class PropertyIri private (smartIri: SmartIri) extends KnoraIri object PropertyIri { + def unsafeFrom(iri: SmartIri): PropertyIri = from(iri).fold(e => throw IllegalArgumentException(e), identity) + + def fromApiV2Complex(iri: SmartIri): Either[String, PropertyIri] = + if iri.isApiV2ComplexSchema then from(iri) + else Left(s"Not an API v2 complex IRI ${iri.toString}") + def from(iri: SmartIri): Either[String, PropertyIri] = - if (!iri.isKnoraEntityIri && iri.isApiV2ComplexSchema) { - Left(s"<$iri> is not a Knora API v2 complex property IRI") - } else { - Right(PropertyIri(iri)) - } + if iri.isKnoraEntityIri then Right(PropertyIri(iri)) + else Left(s"<$iri> is not a Knora property IRI") } final case class ValueIri private ( @@ -46,43 +55,51 @@ object KnoraIris { valueId: ValueId, ) extends KnoraIri + final case class ResourceClassIri private (smartIri: SmartIri, entityName: EntityName) extends KnoraIri + + object ResourceClassIri { + def unsafeFrom(iri: SmartIri): ResourceClassIri = from(iri).fold(e => throw IllegalArgumentException(e), identity) + + def fromApiV2Complex(iri: SmartIri): Either[String, ResourceClassIri] = + if iri.isApiV2ComplexSchema then from(iri) + else Left(s"Not an API v2 complex IRI ${iri.toString}") + + def from(iri: SmartIri): Either[String, ResourceClassIri] = + if iri.isKnoraEntityIri then Right(ResourceClassIri(iri, NonEmptyString.unsafeFrom(iri.getEntityName))) + else Left(s"<$iri> is not a Knora resource class IRI") + } + + // `ValueIri` and `ResourceIri` have no different internal representation. + // Thus, we only provide functions which create these from a `SmartIri`. + // The `fromApiV2Complex` is not required as these Iris are not part of the API v2 complex schema. object ValueIri { + + def unsafeFrom(iri: SmartIri): ValueIri = from(iri).fold(e => throw IllegalArgumentException(e), identity) + def from(iri: SmartIri): Either[String, ValueIri] = - if (!iri.isKnoraValueIri) { - Left(s"<$iri> is not a Knora value IRI") - } else { + if iri.isKnoraValueIri then // the following three calls are safe because we checked that the // shortcode, resourceId and valueId are present in isKnoraValueIri val shortcode = iri.getProjectShortcode.getOrElse(throw Exception()) val resourceId = NonEmptyString.unsafeFrom(iri.getResourceID.getOrElse(throw Exception())) val valueId = NonEmptyString.unsafeFrom(iri.getValueID.getOrElse(throw Exception())) Right(ValueIri(iri, shortcode, resourceId, valueId)) - } + else Left(s"<$iri> is not a Knora value IRI") } final case class ResourceIri private (smartIri: SmartIri, shortcode: Shortcode, resourceId: ResourceId) extends KnoraIri object ResourceIri { + + def unsafeFrom(iri: SmartIri): ResourceIri = from(iri).fold(e => throw IllegalArgumentException(e), identity) + def from(iri: SmartIri): Either[String, ResourceIri] = - if (!iri.isKnoraResourceIri) { - Left(s"<$iri> is not a Knora resource IRI") - } else { + if iri.isKnoraResourceIri then // the following two calls are safe because we checked that the // shortcode and resourceId are present in isKnoraResourceIri val shortcode = iri.getProjectShortcode.getOrElse(throw Exception()) val resourceId = NonEmptyString.unsafeFrom(iri.getResourceID.getOrElse(throw Exception())) Right(ResourceIri(iri, shortcode, resourceId)) - } - } - - final case class ResourceClassIri private (smartIri: SmartIri) extends KnoraIri - - object ResourceClassIri { - def from(iri: SmartIri): Either[String, ResourceClassIri] = - if (!iri.isKnoraEntityIri && iri.isApiV2ComplexSchema) { - Left(s"<$iri> is not a Knora resource class IRI") - } else { - Right(ResourceClassIri(iri)) - } + else Left(s"<$iri> is not a Knora resource IRI") } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala index 110e745107..2cd962fd44 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala @@ -10,8 +10,10 @@ import zio.Task import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.common.KnoraIris.PropertyIri import org.knora.webapi.slice.common.repo.service.Repository import org.knora.webapi.slice.resourceinfo.domain.InternalIri @@ -45,4 +47,6 @@ trait OntologyRepo extends Repository[ReadOntologyV2, InternalIri] { def findDirectSubclassesBy(classIri: InternalIri): Task[List[ReadClassInfoV2]] def findAllSubclassesBy(classIri: InternalIri): Task[List[ReadClassInfoV2]] + + def findProperty(propertyIri: PropertyIri): Task[Option[ReadPropertyInfoV2]] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala index 3e65b8cbed..e2fa6d14b5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala @@ -13,10 +13,14 @@ import zio.prelude.ForEachOps import scala.annotation.tailrec +import org.knora.webapi.InternalSchema import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.common.KnoraIris +import org.knora.webapi.slice.common.KnoraIris.* import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.repo.model.OntologyCacheData import org.knora.webapi.slice.resourceinfo.domain.InternalIri @@ -140,6 +144,15 @@ final case class OntologyRepoLive(private val converter: IriConverter, private v case classes => findAllSuperClassesBy(toClassIris(classes), acc ::: classes, cache, upToClassIri) } } + + override def findProperty(propertyIri: PropertyIri): Task[Option[ReadPropertyInfoV2]] = + getCache.map { c => + val iri = propertyIri.smartIri.toOntologySchema(InternalSchema) + for { + ontology <- c.ontologies.get(iri.getOntologyFromEntity) + property <- ontology.properties.get(iri) + } yield property + } } object OntologyRepoLive { diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/createNewDefaultObjectAccessPermission.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/createNewDefaultObjectAccessPermission.scala.txt deleted file mode 100644 index 6a366003d9..0000000000 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/createNewDefaultObjectAccessPermission.scala.txt +++ /dev/null @@ -1,59 +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 - *@ - -@import org.knora.webapi.IRI - -@* - * Creates a new default object access permission. - * @param namedGraphIri the name of the graph into which the new permission will be created. - * @param permissionIri the Iri of the new administrative permission. - * @param permissionClassIri the IRI of the OWL class that the new administrative permission should belong to. - * @param projectIri the project. - * @param maybeGroupIri the group's IRI. - * @param maybeResourceClassIri the resource's class IRI. - * @param maybePropertyIri the property's IRI. - * @param permissions the permission. - * - *@ -@(namedGraphIri: IRI, - permissionIri: IRI, - permissionClassIri: IRI, - projectIri: IRI, - maybeGroupIri: Option[IRI], - maybeResourceClassIri: Option[IRI], - maybePropertyIri: Option[IRI], - permissions: String) - -PREFIX xsd: -PREFIX rdf: -PREFIX rdfs: -PREFIX owl: -PREFIX knora-admin: -PREFIX knora-base: - -INSERT { - GRAPH ?namedGraphIri { - ?permissionIri rdf:type ?permissionClassIri . - - ?permissionIri knora-admin:forProject ?projectIri . - @if(maybeGroupIri.nonEmpty) { - ?permissionIri knora-admin:forGroup <@maybeGroupIri.get> . - } - @if(maybeResourceClassIri.nonEmpty) { - ?permissionIri knora-admin:forResourceClass <@maybeResourceClassIri.get> . - } - @if(maybePropertyIri.nonEmpty) { - ?permissionIri knora-admin:forProperty <@maybePropertyIri.get> . - } - ?permissionIri knora-base:hasPermissions "@permissions"^^xsd:string . - } -} - -WHERE { - BIND(IRI("@namedGraphIri") AS ?namedGraphIri) - BIND(IRI("@permissionIri") AS ?permissionIri) - BIND(IRI("@permissionClassIri") AS ?permissionClassIri) - BIND(IRI("@projectIri") AS ?projectIri) -} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala index c9df575aea..918bc9b8e0 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala @@ -158,7 +158,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -183,7 +183,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -208,7 +208,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -230,7 +230,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -259,7 +259,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -286,7 +286,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -315,7 +315,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -349,7 +349,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -376,7 +376,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -401,7 +401,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -427,7 +427,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -461,7 +461,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -498,7 +498,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -532,7 +532,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -559,7 +559,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -586,7 +586,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -613,7 +613,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -641,7 +641,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, @@ -680,7 +680,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { | }, | "@context": { | "ka": "http://api.knora.org/ontology/knora-api/v2#", - | "ex": "https://example.com/test#", + | "ex": "http://0.0.0.0:3333/ontology/0001/anything/v2#", | "xsd": "http://www.w3.org/2001/XMLSchema#" | } |}""".stripMargin, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraIrisSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraIrisSpec.scala new file mode 100644 index 0000000000..60ac92a1ac --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraIrisSpec.scala @@ -0,0 +1,172 @@ +/* + * 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.common + +import zio.* +import zio.test.* + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.common.KnoraIris.PropertyIri +import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri +import org.knora.webapi.slice.common.KnoraIris.ResourceIri +import org.knora.webapi.slice.common.KnoraIris.ValueIri +import org.knora.webapi.slice.common.KnoraIrisSpec.test +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object KnoraIrisSpec extends ZIOSpecDefault { + + private val converter = ZIO.serviceWithZIO[IriConverter] + + // Some common test values + private val internalPropertyIri = "http://www.knora.org/ontology/0001/anything#hasListItem" + private val apiV2ComplexPropertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasListItem" + + private val internalResourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing" + private val apiV2ComplexResourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing" + + private val valueIri = "http://rdfh.ch/0001/thing-with-history/values/xZisRC3jPkcplt1hQQdb-A" + private val resourceIri = "http://rdfh.ch/080C/Ef9heHjPWDS7dMR_gGax2Q" + + private val propertyIriSuite = suite("PropertyIri")( + suite("from")( + test("should return a PropertyIri") { + val validIris = Seq(internalPropertyIri, apiV2ComplexPropertyIri) + check(Gen.fromIterable(validIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = PropertyIri.from(sIri) + } yield assertTrue(actual.map(_.smartIri) == Right(sIri)) + } + }, + test("should fail for an invalid PropertyIri") { + val invalidIris = Seq("http://example.com/foo#hasBar", valueIri) + check(Gen.fromIterable(invalidIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = PropertyIri.from(sIri) + } yield assertTrue(actual == Left(s"<${sIri.toIri}> is not a Knora property IRI")) + } + }, + ), + suite("fromApiV2Complex")( + test("should fail for an internal IRI") { + val iri = internalPropertyIri + for { + sIri <- converter(_.asSmartIri(iri)) + actual = PropertyIri.fromApiV2Complex(sIri) + } yield assertTrue(actual == Left(s"Not an API v2 complex IRI $iri")) + }, + ), + ) + + private val resourceClassIriSuite = suite("ResourceClassIri")( + suite("from")( + test("should return a ResourceClassIri") { + val validIris = Seq( + // internal ontology + "http://www.knora.org/ontology/0001/anything#Thing", + // external api v2 complex + "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing", + ) + check(Gen.fromIterable(validIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ResourceClassIri.from(sIri) + } yield assertTrue(actual.map(_.smartIri) == Right(sIri)) + } + }, + test("should fail for an invalid ResourceClassIri") { + val invalidIris = Seq( + "http://example.com/ontology#Foo", + "http://0.0.0.0/ontology/0001/anything/v2#Thing", + "http://rdfh.ch/0001/5zCt1EMJKezFUOW_RCB0Gw/values/tdWAtnWK2qUC6tr4uQLAHA", + ) + check(Gen.fromIterable(invalidIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ResourceClassIri.from(sIri) + } yield assertTrue(actual == Left(s"<${sIri.toIri}> is not a Knora resource class IRI")) + } + }, + ), + suite("fromApiV2Complex")( + test("should fail for an internal IRI") { + val iri = "http://www.knora.org/ontology/0001/anything#Thing" + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ResourceClassIri.fromApiV2Complex(sIri) + } yield assertTrue(actual == Left(s"Not an API v2 complex IRI $iri")) + }, + ), + ) + + private val valueIriSuite = suite("ValueIri")( + suite("from")( + test("should return a ValueIri") { + val validIris = Seq(valueIri) + check(Gen.fromIterable(validIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ValueIri.from(sIri) + } yield assertTrue(actual.map(_.smartIri) == Right(sIri)) + } + }, + test("should fail for an invalid ValueIri") { + val invalidIris = Seq( + "http://example.com/ontology#Foo", + internalResourceClassIri, + apiV2ComplexResourceClassIri, + internalPropertyIri, + apiV2ComplexPropertyIri, + resourceIri, + ) + check(Gen.fromIterable(invalidIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ValueIri.from(sIri) + } yield assertTrue(actual == Left(s"<${sIri.toIri}> is not a Knora value IRI")) + } + }, + ), + ) + + private val resourceIriSuite = suite("ResourceIri")( + suite("from")( + test("should return a ResourceIri") { + val validIris = Seq(resourceIri) + check(Gen.fromIterable(validIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ResourceIri.from(sIri) + } yield assertTrue(actual.map(_.smartIri) == Right(sIri)) + } + }, + test("should fail for an invalid ResourceIri") { + val invalidIris = Seq( + "http://example.com/ontology#Foo", + internalResourceClassIri, + apiV2ComplexResourceClassIri, + internalPropertyIri, + apiV2ComplexPropertyIri, + valueIri, + ) + check(Gen.fromIterable(invalidIris)) { iri => + for { + sIri <- converter(_.asSmartIri(iri)) + actual = ResourceIri.from(sIri) + } yield assertTrue(actual == Left(s"<${sIri.toIri}> is not a Knora resource IRI")) + } + }, + ), + ) + + val spec = suite("KnoraIris")( + resourceClassIriSuite, + resourceIriSuite, + propertyIriSuite, + valueIriSuite, + ).provide(IriConverter.layer, StringFormatter.test) +}