From 88e6b7295d9db579d5df5c55e20bd2565e500dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 19 Jun 2024 13:11:47 +0200 Subject: [PATCH] fix: Add prefix handling to admin permission repo (#3290) --- .../AdministrativePermissionRepoLive.scala | 8 +- .../triplestore/api/TriplestoreService.scala | 3 + ...AdministrativePermissionRepoLiveSpec.scala | 105 ++++++++++++++++++ ...ltObjectAccessPermissionRepoLiveSpec.scala | 97 ++++++++++++++++ 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLiveSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLiveSpec.scala diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLive.scala index 2a1e96e6a6..3300ac4075 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLive.scala @@ -17,6 +17,8 @@ import zio.ZIO import zio.ZLayer import org.knora.webapi.messages.OntologyConstants.KnoraAdmin +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.KnoraAdminPrefix +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.KnoraAdminPrefixExpansion import org.knora.webapi.messages.OntologyConstants.KnoraBase import org.knora.webapi.slice.admin.AdminConstants.permissionsDataNamedGraph import org.knora.webapi.slice.admin.domain.model.AdministrativePermission @@ -125,14 +127,16 @@ object AdministrativePermissionRepoLive { .andHas(Vocabulary.KnoraAdmin.forProject, Rdf.iri(entity.forProject.value)) .andHas(Vocabulary.KnoraBase.hasPermissions, toStringLiteral(entity.permissions)) } - private def toStringLiteral(permissions: Chunk[AdministrativePermissionPart]): String = + private def toStringLiteral(permissions: Chunk[AdministrativePermissionPart]): String = { + def useKnoraAdminPrefix(str: String) = str.replace(KnoraAdminPrefixExpansion, KnoraAdminPrefix) permissions.map { case AdministrativePermissionPart.Simple(permission) => permission.token case AdministrativePermissionPart.ResourceCreateRestricted(iris) => s"${Permission.Administrative.ProjectResourceCreateRestricted.token} ${iris.map(_.value).mkString(",")}" case AdministrativePermissionPart.ProjectAdminGroupRestricted(groups) => - s"${Permission.Administrative.ProjectAdminGroupRestricted.token} ${groups.map(_.value).mkString(",")}" + s"${Permission.Administrative.ProjectAdminGroupRestricted.token} ${groups.map(_.value).map(useKnoraAdminPrefix).mkString(",")}" }.mkString(permissionsDelimiter.toString) + } } val layer = ZLayer.succeed(mapper) >>> ZLayer.derive[AdministrativePermissionRepoLive] diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/api/TriplestoreService.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/api/TriplestoreService.scala index 2f6c001248..2cfcc5affe 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/api/TriplestoreService.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/api/TriplestoreService.scala @@ -8,6 +8,7 @@ package org.knora.webapi.store.triplestore.api import org.eclipse.rdf4j.sparqlbuilder.core.query.ConstructQuery import org.eclipse.rdf4j.sparqlbuilder.core.query.InsertDataQuery import org.eclipse.rdf4j.sparqlbuilder.core.query.ModifyQuery +import org.eclipse.rdf4j.sparqlbuilder.core.query.SelectQuery import play.twirl.api.TxtFormat import zio.* @@ -57,6 +58,7 @@ trait TriplestoreService { * @return A [[SparqlSelectResult]]. */ def query(sparql: Select): Task[SparqlSelectResult] + final def select(sparql: SelectQuery): Task[SparqlSelectResult] = query(Select(sparql)) /** * Performs a SPARQL update operation, i.e. an INSERT or DELETE query. @@ -169,6 +171,7 @@ object TriplestoreService { case class Select(sparql: String, override val timeout: SparqlTimeout = SparqlTimeout.Standard) extends SparqlQuery object Select { + def apply(sparql: SelectQuery): Select = Select(sparql.getQueryString) def apply(sparql: TxtFormat.Appendable): Select = Select(sparql.toString) def gravsearch(sparql: TxtFormat.Appendable): Select = Select.gravsearch(sparql.toString) diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLiveSpec.scala new file mode 100644 index 0000000000..fed05be65f --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/AdministrativePermissionRepoLiveSpec.scala @@ -0,0 +1,105 @@ +/* + * 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.repo.service + +import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable +import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries +import org.eclipse.rdf4j.sparqlbuilder.core.query.SelectQuery +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf +import zio.Chunk +import zio.ZIO +import zio.test.* + +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.KnoraAdminPrefixExpansion +import org.knora.webapi.messages.StringFormatter +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.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode +import org.knora.webapi.slice.admin.domain.model.Permission.Administrative.* +import org.knora.webapi.slice.admin.domain.model.PermissionIri +import org.knora.webapi.slice.admin.repo.rdf.Vocabulary +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.store.triplestore.api.TriplestoreService +import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory + +object AdministrativePermissionRepoLiveSpec extends ZIOSpecDefault { + private val repo = ZIO.serviceWithZIO[AdministrativePermissionRepo] + private val db = ZIO.serviceWithZIO[TriplestoreService] + private val shortcode = Shortcode.unsafeFrom("0001") + private val groupIri = GroupIri.makeNew(shortcode) + private val projectIri = ProjectIri.makeNew + private def permission(permissions: Chunk[AdministrativePermissionPart]) = + AdministrativePermission(PermissionIri.makeNew(shortcode), groupIri, projectIri, permissions) + + private val simpleAdminPermission = permission( + Chunk(AdministrativePermissionPart.Simple.unsafeFrom(ProjectResourceCreateAll)), + ) + private val complexAdminPermission = permission( + Chunk( + AdministrativePermissionPart.ProjectAdminGroupRestricted(Chunk(groupIri, GroupIri.makeNew(shortcode))), + AdministrativePermissionPart.ResourceCreateRestricted( + Chunk(InternalIri("https://example.org/1"), InternalIri("https://example.org/2")), + ), + ), + ) + + val spec = suite("AdministrativePermissionRepoLive")( + test("should save and find") { + for { + saved <- repo(_.save(simpleAdminPermission)) + found <- repo(_.findById(saved.id)) + } yield assertTrue(found.contains(saved), saved == simpleAdminPermission) + }, + test("should handle complex permission parts") { + val expected = complexAdminPermission + for { + saved <- repo(_.save(expected)) + found <- repo(_.findById(saved.id)) + } yield assertTrue(found.contains(saved), saved == expected) + }, + test("should delete") { + val expected = complexAdminPermission + for { + saved <- repo(_.save(expected)) + foundAfterSave <- repo(_.findById(saved.id)).map(_.nonEmpty) + _ <- repo(_.delete(expected)) + notfoundAfterDelete <- repo(_.findById(saved.id)).map(_.isEmpty) + } yield assertTrue(foundAfterSave, notfoundAfterDelete) + }, + test("should write valid permission literal with the 'knora-admin:' prefix") { + val expected = permission( + Chunk( + AdministrativePermissionPart.Simple.unsafeFrom(ProjectResourceCreateAll), + AdministrativePermissionPart.ProjectAdminGroupRestricted( + Chunk( + GroupIri.unsafeFrom(KnoraAdminPrefixExpansion + "Creator"), + GroupIri.unsafeFrom(KnoraAdminPrefixExpansion + "UnknownUser"), + groupIri, + ), + ), + ), + ) + def query(id: PermissionIri): SelectQuery = { + val hasPermissionsLiteral = variable("lit") + Queries + .SELECT() + .select(hasPermissionsLiteral) + .where(Rdf.iri(id.value).has(Vocabulary.KnoraBase.hasPermissions, hasPermissionsLiteral)) + } + for { + saved <- repo(_.save(expected)) + res <- db(_.select(query(saved.id))) + } yield assertTrue( + res + .getFirst("lit") + .head == s"ProjectResourceCreateAllPermission|ProjectAdminGroupRestrictedPermission knora-admin:Creator,knora-admin:UnknownUser,${groupIri.value}", + ) + }, + ).provide(AdministrativePermissionRepoLive.layer, TriplestoreServiceInMemory.emptyLayer, StringFormatter.test) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLiveSpec.scala new file mode 100644 index 0000000000..cbe5e1a1e1 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/DefaultObjectAccessPermissionRepoLiveSpec.scala @@ -0,0 +1,97 @@ +/* + * 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.repo.service + +import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable +import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries +import org.eclipse.rdf4j.sparqlbuilder.core.query.SelectQuery +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf +import zio.Chunk +import zio.NonEmptyChunk +import zio.ZIO +import zio.test.* + +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.KnoraAdminPrefixExpansion +import org.knora.webapi.messages.StringFormatter +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.GroupIri +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.Permission.ObjectAccess.* +import org.knora.webapi.slice.admin.domain.model.PermissionIri +import org.knora.webapi.slice.admin.repo.rdf.Vocabulary +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.store.triplestore.api.TriplestoreService +import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory + +object DefaultObjectAccessPermissionRepoLiveSpec extends ZIOSpecDefault { + private val repo = ZIO.serviceWithZIO[DefaultObjectAccessPermissionRepo] + private val db = ZIO.serviceWithZIO[TriplestoreService] + private val shortcode = Shortcode.unsafeFrom("0001") + private val groupIri = GroupIri.makeNew(shortcode) + private val projectIri = ProjectIri.makeNew + + private def permission(forWhat: ForWhat, permissions: Chunk[DefaultObjectAccessPermissionPart]) = + DefaultObjectAccessPermission(PermissionIri.makeNew(shortcode), projectIri, forWhat, permissions) + + private val expected = permission( + ForWhat.ResourceClassAndProperty(InternalIri("https://example.com/rc"), InternalIri("https://example.com/p")), + Chunk( + DefaultObjectAccessPermissionPart(RestrictedView, NonEmptyChunk(groupIri)), + DefaultObjectAccessPermissionPart(View, NonEmptyChunk(GroupIri.makeNew(shortcode), GroupIri.makeNew(shortcode))), + ), + ) + + val spec = suite("AdministrativePermissionRepoLive")( + test("should save and find") { + for { + saved <- repo(_.save(expected)) + found <- repo(_.findById(saved.id)) + } yield assertTrue(found.contains(saved), saved == expected) + }, + test("should delete") { + for { + saved <- repo(_.save(expected)) + foundAfterSave <- repo(_.findById(saved.id)).map(_.nonEmpty) + _ <- repo(_.delete(expected)) + notfoundAfterDelete <- repo(_.findById(saved.id)).map(_.isEmpty) + } yield assertTrue(foundAfterSave, notfoundAfterDelete) + }, + test("should write valid permission literal with the 'knora-admin:' prefix") { + val expected = permission( + ForWhat.Group(groupIri), + Chunk( + DefaultObjectAccessPermissionPart(RestrictedView, NonEmptyChunk(groupIri)), + DefaultObjectAccessPermissionPart( + View, + NonEmptyChunk( + GroupIri.unsafeFrom(KnoraAdminPrefixExpansion + "Creator"), + GroupIri.unsafeFrom(KnoraAdminPrefixExpansion + "UnknownUser"), + ), + ), + ), + ) + def query(id: PermissionIri): SelectQuery = { + val hasPermissionsLiteral = variable("lit") + Queries + .SELECT() + .select(hasPermissionsLiteral) + .where(Rdf.iri(id.value).has(Vocabulary.KnoraBase.hasPermissions, hasPermissionsLiteral)) + } + for { + saved <- repo(_.save(expected)) + res <- db(_.select(query(saved.id))) + } yield assertTrue( + res + .getFirst("lit") + .head == s"RV ${groupIri.value}|V knora-admin:Creator,knora-admin:UnknownUser", + ) + }, + ).provide(DefaultObjectAccessPermissionRepoLive.layer, TriplestoreServiceInMemory.emptyLayer, StringFormatter.test) +}