diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala index dc0c4020dd..d78cd1be85 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala @@ -8,7 +8,6 @@ package org.knora.webapi.responders.admin import java.util.UUID import dsp.errors.* -import dsp.valueobjects.Group.* import dsp.valueobjects.V2 import org.knora.webapi.* import org.knora.webapi.messages.admin.responder.groupsmessages.* @@ -19,7 +18,11 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM.* import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest +import org.knora.webapi.slice.admin.domain.model.GroupDescriptions import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupName +import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin +import org.knora.webapi.slice.admin.domain.model.GroupStatus import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.util.MutableTestIri import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA @@ -71,7 +74,7 @@ class GroupsResponderADMSpec extends CoreSpec { ), project = ProjectIri.unsafeFrom(imagesProjectIri), status = GroupStatus.active, - selfjoin = GroupSelfJoin.impossible + selfjoin = GroupSelfJoin.disabled ), UUID.randomUUID ) @@ -101,7 +104,7 @@ class GroupsResponderADMSpec extends CoreSpec { .unsafeFrom(Seq(V2.StringLiteralV2(value = "NewGroupDescription", language = Some("en")))), project = ProjectIri.unsafeFrom(imagesProjectIri), status = GroupStatus.active, - selfjoin = GroupSelfJoin.impossible + selfjoin = GroupSelfJoin.disabled ), UUID.randomUUID ) @@ -124,7 +127,7 @@ class GroupsResponderADMSpec extends CoreSpec { ) ), status = Some(GroupStatus.active), - selfjoin = Some(GroupSelfJoin.impossible) + selfjoin = Some(GroupSelfJoin.disabled) ), UUID.randomUUID ) @@ -151,7 +154,7 @@ class GroupsResponderADMSpec extends CoreSpec { .unsafeFrom(Seq(V2.StringLiteralV2(value = "UpdatedDescription", language = Some("en")))) ), status = Some(GroupStatus.active), - selfjoin = Some(GroupSelfJoin.impossible) + selfjoin = Some(GroupSelfJoin.disabled) ), UUID.randomUUID ) @@ -174,7 +177,7 @@ class GroupsResponderADMSpec extends CoreSpec { .unsafeFrom(Seq(V2.StringLiteralV2(value = "UpdatedDescription", language = Some("en")))) ), status = Some(GroupStatus.active), - selfjoin = Some(GroupSelfJoin.impossible) + selfjoin = Some(GroupSelfJoin.disabled) ), UUID.randomUUID ) diff --git a/webapi/src/main/scala/dsp/valueobjects/Group.scala b/webapi/src/main/scala/dsp/valueobjects/Group.scala deleted file mode 100644 index 71668e2639..0000000000 --- a/webapi/src/main/scala/dsp/valueobjects/Group.scala +++ /dev/null @@ -1,68 +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 dsp.valueobjects - -import org.knora.webapi.slice.common.StringValueCompanion -import org.knora.webapi.slice.common.Value -import org.knora.webapi.slice.common.Value.BooleanValue -import org.knora.webapi.slice.common.Value.StringValue -import org.knora.webapi.slice.common.WithFrom - -object Group { - - /** - * GroupName value object. - */ - final case class GroupName private (value: String) extends AnyVal with StringValue - object GroupName extends StringValueCompanion[GroupName] { - def from(value: String): Either[String, GroupName] = - value match { - case _ if value.isEmpty => Left(GroupErrorMessages.GroupNameMissing) - case _ => - Iri - .toSparqlEncodedString(value) - .toRight(GroupErrorMessages.GroupNameInvalid) - .map(GroupName.apply) - } - } - - final case class GroupDescriptions private (override val value: Seq[V2.StringLiteralV2]) - extends AnyVal - with Value[Seq[V2.StringLiteralV2]] - object GroupDescriptions extends WithFrom[Seq[V2.StringLiteralV2], GroupDescriptions] { - def from(value: Seq[V2.StringLiteralV2]): Either[String, GroupDescriptions] = - if (value.isEmpty) Left(GroupErrorMessages.GroupDescriptionsMissing) - else - Iri - .toSparqlEncodedString(value.head.value) - .toRight(GroupErrorMessages.GroupDescriptionsInvalid) - .map(_ => GroupDescriptions(value)) - } - - /** - * GroupStatus value object. - */ - final case class GroupStatus private (value: Boolean) extends AnyVal with BooleanValue - object GroupStatus { - val active: GroupStatus = new GroupStatus(true) - val inactive: GroupStatus = new GroupStatus(false) - def from(value: Boolean): GroupStatus = if (value) active else inactive - } - - final case class GroupSelfJoin private (value: Boolean) extends AnyVal with BooleanValue - object GroupSelfJoin { - val possible: GroupSelfJoin = new GroupSelfJoin(true) - val impossible: GroupSelfJoin = new GroupSelfJoin(false) - def from(value: Boolean): GroupSelfJoin = if (value) possible else impossible - } -} - -object GroupErrorMessages { - val GroupNameMissing = "Group name cannot be empty." - val GroupNameInvalid = "Group name is invalid." - val GroupDescriptionsMissing = "Group description cannot be empty." - val GroupDescriptionsInvalid = "Group description is invalid." -} diff --git a/webapi/src/main/scala/dsp/valueobjects/V2.scala b/webapi/src/main/scala/dsp/valueobjects/V2.scala index f87baf5d65..cbc63d177a 100644 --- a/webapi/src/main/scala/dsp/valueobjects/V2.scala +++ b/webapi/src/main/scala/dsp/valueobjects/V2.scala @@ -5,6 +5,8 @@ package dsp.valueobjects +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfLiteral.StringLiteral import zio.json.* object V2 { @@ -15,7 +17,11 @@ object V2 { * @param value the string value. * @param language the language iso. */ - case class StringLiteralV2(value: String, language: Option[String]) + case class StringLiteralV2(value: String, language: Option[String]) { + def toRdfLiteral: StringLiteral = + language.map(Rdf.literalOfLanguage(value, _)).getOrElse(Rdf.literalOf(value)) + } + object StringLiteralV2 { implicit val codec: JsonCodec[StringLiteralV2] = DeriveJsonCodec.gen[StringLiteralV2] } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index c05d274aca..47d4998f52 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -465,7 +465,7 @@ object OntologyConstants { val IsInGroup: IRI = KnoraAdminPrefixExpansion + "isInGroup" val IsInSystemAdminGroup: IRI = KnoraAdminPrefixExpansion + "isInSystemAdminGroup" - /* Status used for User and Project*/ + /* Status used for User, Group and Project */ val StatusProp: IRI = KnoraAdminPrefixExpansion + "status" /* Project */ diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala index 4785690736..6a46a5371f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala @@ -12,7 +12,6 @@ import zio.macros.accessible import java.util.UUID import dsp.errors.* -import dsp.valueobjects.Group.GroupStatus import org.knora.webapi.* import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay @@ -40,6 +39,7 @@ import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupStatus import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala index e5b752a9f8..7fc1e805e8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala @@ -9,28 +9,15 @@ import sttp.tapir.Codec import sttp.tapir.CodecFormat import zio.json.JsonCodec -import dsp.valueobjects.Group.GroupDescriptions -import dsp.valueobjects.Group.GroupName -import dsp.valueobjects.Group.GroupSelfJoin -import dsp.valueobjects.Group.GroupStatus import dsp.valueobjects.V2 import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId -import org.knora.webapi.slice.admin.domain.model.Email -import org.knora.webapi.slice.admin.domain.model.FamilyName -import org.knora.webapi.slice.admin.domain.model.GivenName -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.ListProperties.Comments import org.knora.webapi.slice.admin.domain.model.ListProperties.Labels import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName import org.knora.webapi.slice.admin.domain.model.ListProperties.Position -import org.knora.webapi.slice.admin.domain.model.Password -import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize -import org.knora.webapi.slice.admin.domain.model.SystemAdmin -import org.knora.webapi.slice.admin.domain.model.UserIri -import org.knora.webapi.slice.admin.domain.model.UserStatus -import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.slice.common.Value.BooleanValue import org.knora.webapi.slice.common.Value.IntValue import org.knora.webapi.slice.common.Value.StringValue diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala index 363e9cc92e..7262696635 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala @@ -13,10 +13,6 @@ import zio.* import zio.json.DeriveJsonCodec import zio.json.JsonCodec -import dsp.valueobjects.Group.GroupDescriptions -import dsp.valueobjects.Group.GroupName -import dsp.valueobjects.Group.GroupSelfJoin -import dsp.valueobjects.Group.GroupStatus import org.knora.webapi.messages.admin.responder.groupsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.GroupMembersGetResponseADM import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol.* @@ -24,7 +20,11 @@ import org.knora.webapi.slice.admin.api.AdminPathVariables.groupIriPathVar import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest +import org.knora.webapi.slice.admin.domain.model.GroupDescriptions import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupName +import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin +import org.knora.webapi.slice.admin.domain.model.GroupStatus import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.BaseEndpoints diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala deleted file mode 100644 index 3e263992b3..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala +++ /dev/null @@ -1,58 +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.admin.domain.model - -import sttp.tapir.Codec -import sttp.tapir.CodecFormat - -import dsp.valueobjects.Iri -import dsp.valueobjects.UuidUtil -import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.BuiltInGroups -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.common.StringValueCompanion -import org.knora.webapi.slice.common.Value.StringValue - -final case class KnoraUserGroup(id: GroupIri, belongsToProject: ProjectIri) - -final case class GroupIri private (override val value: String) extends AnyVal with StringValue - -object GroupIri extends StringValueCompanion[GroupIri] { - - implicit val tapirCodec: Codec[String, GroupIri, CodecFormat.TextPlain] = - Codec.string.mapEither(GroupIri.from)(_.value) - - /** - * Explanation of the group IRI regex: - * `^` asserts the start of the string. - * `http://rdfh\.ch/groups/` matches the specified prefix. - * `p{XDigit}{4}/` matches project shortcode built with 4 hexadecimal digits. - * `[a-zA-Z0-9_-]{4,40}` matches any alphanumeric character, hyphen, or underscore between 4 and 40 times. - * TODO: 30 was max length found on production DBs, but increased it to 40 - some tests may fail. - * `$` asserts the end of the string. - */ - private val groupIriRegEx = """^http://rdfh\.ch/groups/\p{XDigit}{4}/[a-zA-Z0-9_-]{4,40}$""".r - - private def isGroupIriValid(iri: String): Boolean = - (Iri.isIri(iri) && groupIriRegEx.matches(iri)) || BuiltInGroups.contains(iri) - - def from(value: String): Either[String, GroupIri] = value match { - case _ if value.isEmpty => Left("Group IRI cannot be empty.") - case _ if isGroupIriValid(value) => Right(GroupIri(value)) - case _ => Left("Group IRI is invalid.") - } - - /** - * Creates a new group IRI based on a UUID. - * - * @param shortcode the shortcode of a project the group belongs to. - * @return a new group IRI. - */ - def makeNew(shortcode: Shortcode): GroupIri = { - val uuid = UuidUtil.makeRandomBase64EncodedUuid - unsafeFrom(s"http://rdfh.ch/groups/${shortcode.value}/$uuid") - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroup.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroup.scala new file mode 100644 index 0000000000..9150d8ac66 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroup.scala @@ -0,0 +1,136 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.admin.domain.model + +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfLiteral.StringLiteral +import sttp.tapir.Codec +import sttp.tapir.CodecFormat + +import dsp.valueobjects.Iri +import dsp.valueobjects.UuidUtil +import dsp.valueobjects.V2 +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.BuiltInGroups +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.common.StringValueCompanion +import org.knora.webapi.slice.common.Value +import org.knora.webapi.slice.common.Value.BooleanValue +import org.knora.webapi.slice.common.Value.StringValue +import org.knora.webapi.slice.common.WithFrom +import org.knora.webapi.slice.common.repo.rdf.LangString + +/** + * The user entity as found in the knora-admin ontology. + */ +final case class KnoraUserGroup( + id: GroupIri, + groupName: GroupName, + groupDescriptions: GroupDescriptions, + status: GroupStatus, + belongsToProject: Option[ProjectIri], + hasSelfJoinEnabled: GroupSelfJoin +) + +final case class GroupIri private (override val value: String) extends AnyVal with StringValue + +object GroupIri extends StringValueCompanion[GroupIri] { + implicit val tapirCodec: Codec[String, GroupIri, CodecFormat.TextPlain] = + Codec.string.mapEither(GroupIri.from)(_.value) + + /** + * Explanation of the group IRI regex: + * `^` asserts the start of the string. + * `http://rdfh\.ch/groups/` matches the specified prefix. + * `p{XDigit}{4}/` matches project shortcode built with 4 hexadecimal digits. + * `[a-zA-Z0-9_-]{4,40}` matches any alphanumeric character, hyphen, or underscore between 4 and 40 times. + * TODO: 30 was max length found on production DBs, but increased it to 40 - some tests may fail. + * `$` asserts the end of the string. + */ + private val groupIriRegEx = """^http://rdfh\.ch/groups/\p{XDigit}{4}/[a-zA-Z0-9_-]{4,40}$""".r + + private def isGroupIriValid(iri: String): Boolean = + (Iri.isIri(iri) && groupIriRegEx.matches(iri)) || BuiltInGroups.contains(iri) + + def from(value: String): Either[String, GroupIri] = value match { + case _ if value.isEmpty => Left("Group IRI cannot be empty.") + case _ if isGroupIriValid(value) => Right(GroupIri(value)) + case _ => Left("Group IRI is invalid.") + } + + /** + * Creates a new group IRI based on a UUID. + * + * @param shortcode the shortcode of a project the group belongs to. + * @return a new group IRI. + */ + def makeNew(shortcode: Shortcode): GroupIri = { + val uuid = UuidUtil.makeRandomBase64EncodedUuid + unsafeFrom(s"http://rdfh.ch/groups/${shortcode.value}/$uuid") + } +} + +final case class GroupName private (value: String) extends AnyVal with StringValue + +object GroupName extends StringValueCompanion[GroupName] { + def from(value: String): Either[String, GroupName] = + Right(GroupName(value)).filterOrElse(_.value.nonEmpty, GroupErrorMessages.GroupNameMissing) +} + +final case class GroupDescriptions private (value: Seq[V2.StringLiteralV2]) + extends AnyVal + with Value[Seq[V2.StringLiteralV2]] { + def toRdfLiterals: Seq[StringLiteral] = value.map(_.toRdfLiteral) +} + +object GroupDescriptions extends WithFrom[Seq[V2.StringLiteralV2], GroupDescriptions] { + def from(value: Seq[V2.StringLiteralV2]): Either[String, GroupDescriptions] = + value.toList match { + case descriptions @ (v2String :: _) if v2String.value.nonEmpty => Right(GroupDescriptions(descriptions)) + case _ :: _ => Left(GroupErrorMessages.GroupDescriptionsInvalid) + case _ => Left(GroupErrorMessages.GroupDescriptionsMissing) + } + + def fromOne(value: V2.StringLiteralV2): Either[String, V2.StringLiteralV2] = + Some(value).filter(_.value.nonEmpty).toRight(GroupErrorMessages.GroupDescriptionsInvalid) + +} + +/** + * GroupStatus value object. + */ +final case class GroupStatus private (value: Boolean) extends AnyVal with BooleanValue + +object GroupStatus { + val active: GroupStatus = GroupStatus(true) + val inactive: GroupStatus = GroupStatus(false) + def from(active: Boolean): GroupStatus = GroupStatus(active) +} + +final case class GroupSelfJoin private (value: Boolean) extends AnyVal with BooleanValue +object GroupSelfJoin { + val enabled: GroupSelfJoin = GroupSelfJoin(true) + val disabled: GroupSelfJoin = GroupSelfJoin(false) + def from(enabled: Boolean): GroupSelfJoin = GroupSelfJoin(enabled) +} + +object KnoraUserGroup { + object Conversions { + implicit val groupIriConverter: String => Either[String, GroupIri] = GroupIri.from + implicit val groupNameConverter: String => Either[String, GroupName] = GroupName.from + implicit val groupDescriptionsConverter: LangString => Either[String, V2.StringLiteralV2] = langString => + GroupDescriptions.fromOne(V2.StringLiteralV2(langString.value, langString.lang)) + implicit val groupStatusConverter: Boolean => Either[String, GroupStatus] = value => Right(GroupStatus.from(value)) + implicit val groupHasSelfJoinEnabledConverter: Boolean => Either[String, GroupSelfJoin] = value => + Right(GroupSelfJoin.from(value)) + } +} + +object GroupErrorMessages { + val GroupNameMissing = "Group name cannot be empty." + val GroupNameInvalid = "Group name is invalid." + val GroupDescriptionsMissing = "Group description cannot be empty." + val GroupDescriptionsInvalid = "Group description is invalid." +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserGroupRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserGroupRepo.scala index 96e40ea48f..690f11135c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserGroupRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserGroupRepo.scala @@ -5,8 +5,19 @@ package org.knora.webapi.slice.admin.domain.service +import zio.Task + import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraUserGroup import org.knora.webapi.slice.common.repo.service.Repository -trait KnoraUserGroupRepo extends Repository[KnoraUserGroup, GroupIri] {} +trait KnoraUserGroupRepo extends Repository[KnoraUserGroup, GroupIri] { + + /** + * Saves the user group, returns the created data. Updates not supported. + * + * @param user The [[KnoraUserGroup]] to be saved, can be an update or a creation. + * @return the saved entity. + */ + def save(userGroup: KnoraUserGroup): Task[KnoraUserGroup] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala index 4239968c72..395c7f2b08 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala @@ -35,9 +35,12 @@ object Vocabulary { val isInGroup: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "isInGroup") val isInSystemAdminGroup: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "isInSystemAdminGroup") val isInProjectAdminGroup: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "isInProjectAdminGroup") + val hasSelfJoinEnabled: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "hasSelfJoinEnabled") // user group properties - val belongsToProject: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "belongsToProject") + val belongsToProject: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "belongsToProject") + val groupName: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "groupName") + val groupDescriptions: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "groupDescriptions") } object KnoraBase { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLive.scala index e268a148ea..6e8ea9b625 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLive.scala @@ -6,59 +6,79 @@ package org.knora.webapi.slice.admin.repo.service import org.eclipse.rdf4j.model.vocabulary.RDF +import org.eclipse.rdf4j.model.vocabulary.* import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.prefix import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable import org.eclipse.rdf4j.sparqlbuilder.core.query.ConstructQuery +import org.eclipse.rdf4j.sparqlbuilder.core.query.ModifyQuery import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPatterns.tp +import org.eclipse.rdf4j.sparqlbuilder.rdf.Iri import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf import zio.Task import zio.ZIO import zio.ZLayer import zio.stream.ZStream -import org.knora.webapi.messages.OntologyConstants +import dsp.valueobjects.V2 +import org.knora.webapi.messages.OntologyConstants.KnoraAdmin +import org.knora.webapi.slice.admin.AdminConstants.adminDataNamedGraph import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupStatus import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.admin.domain.model.KnoraUserGroup +import org.knora.webapi.slice.admin.domain.model.KnoraUserGroup.Conversions.* +import org.knora.webapi.slice.admin.domain.model._ import org.knora.webapi.slice.admin.domain.service.KnoraUserGroupRepo +import org.knora.webapi.slice.admin.repo.rdf.RdfConversions.projectIriConverter import org.knora.webapi.slice.admin.repo.rdf.Vocabulary import org.knora.webapi.slice.common.repo.rdf.RdfResource import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update import org.knora.webapi.store.triplestore.errors.TriplestoreResponseException final case class KnoraUserGroupRepoLive(triplestore: TriplestoreService) extends KnoraUserGroupRepo { - override def findById(id: GroupIri): Task[Option[KnoraUserGroup]] = for { - model <- triplestore.queryRdfModel(UserGroupQueries.findById(id)) - resource <- model.getResource(id.value) - user <- ZIO.foreach(resource)(toGroup) - } yield user + model <- triplestore.queryRdfModel(KnoraUserGroupQueries.findById(id)) + resource <- model.getResource(id.value) + userGroup <- ZIO.foreach(resource)(toGroup) + } yield userGroup override def findAll(): Task[List[KnoraUserGroup]] = for { - model <- triplestore.queryRdfModel(UserGroupQueries.findAll) + model <- triplestore.queryRdfModel(KnoraUserGroupQueries.findAll) resources <- - model.getResourcesRdfType(OntologyConstants.KnoraAdmin.UserGroup).option.map(_.getOrElse(Iterator.empty)) + model.getResourcesRdfType(KnoraAdmin.UserGroup).option.map(_.getOrElse(Iterator.empty)) groups <- ZStream.fromIterator(resources).mapZIO(toGroup).runCollect } yield groups.toList - private def toGroup(resource: RdfResource): Task[KnoraUserGroup] = for { - id <- - resource.iri.flatMap(it => ZIO.fromEither(GroupIri.from(it.value))).mapError(TriplestoreResponseException.apply) - belongsToProject <- resource - .getObjectIriOrFail(OntologyConstants.KnoraAdmin.BelongsToProject) - .flatMap(it => ZIO.fromEither(ProjectIri.from(it.value))) - .mapError(it => TriplestoreResponseException(it.toString)) - } yield KnoraUserGroup(id, belongsToProject) + def save(userGroup: KnoraUserGroup): Task[KnoraUserGroup] = + triplestore.query(KnoraUserGroupQueries.save(userGroup)).as(userGroup) + + private def toGroup(resource: RdfResource): Task[KnoraUserGroup] = { + for { + id <- resource.iri.flatMap(it => ZIO.fromEither(GroupIri.from(it.value))) + groupName <- resource.getStringLiteralOrFail[GroupName](KnoraAdmin.GroupName) + groupDescriptions <- resource.getLangStringLiteralsOrFail[V2.StringLiteralV2](KnoraAdmin.GroupDescriptions) + groupDescriptions <- ZIO.fromEither(GroupDescriptions.from(groupDescriptions)) + groupStatus <- resource.getBooleanLiteralOrFail[GroupStatus](KnoraAdmin.StatusProp) + belongsToProject <- resource.getObjectIrisConvert[ProjectIri](KnoraAdmin.BelongsToProject).map(_.headOption) + hasSelfJoinEnabled <- resource.getBooleanLiteralOrFail[GroupSelfJoin](KnoraAdmin.HasSelfJoinEnabled) + } yield KnoraUserGroup( + id, + groupName, + groupDescriptions, + groupStatus, + belongsToProject, + hasSelfJoinEnabled + ) + }.mapError(it => TriplestoreResponseException(it.toString)) } object KnoraUserGroupRepoLive { val layer = ZLayer.derive[KnoraUserGroupRepoLive] } -object UserGroupQueries { - +private object KnoraUserGroupQueries { def findAll: Construct = { val (s, p, o) = (variable("s"), variable("p"), variable("o")) val query = Queries @@ -70,7 +90,6 @@ object UserGroupQueries { .and(s.has(p, o)) .from(Vocabulary.NamedGraphs.knoraAdminIri) ) - println(query.getQueryString) Construct(query.getQueryString) } @@ -86,7 +105,51 @@ object UserGroupQueries { .and(tp(s, p, o)) .from(Vocabulary.NamedGraphs.knoraAdminIri) ) - println(query.getQueryString) Construct(query) } + + private def deleteWhere( + id: Iri, + rdfType: Iri, + query: ModifyQuery, + iris: List[Iri] + ): ModifyQuery = + query + .delete(iris.zipWithIndex.foldLeft(id.has(RDF.TYPE, rdfType)) { case (p, (iri, index)) => + p.andHas(iri, variable(s"n${index}")) + }) + .where(iris.zipWithIndex.foldLeft(id.has(RDF.TYPE, rdfType).optional()) { case (p, (iri, index)) => + p.and(id.has(iri, variable(s"n${index}")).optional()) + }) + + def save(group: KnoraUserGroup): Update = { + val query: ModifyQuery = + Queries + .MODIFY() + .prefix(prefix(RDF.NS), prefix(Vocabulary.KnoraAdmin.NS), prefix(XSD.NS)) + .`with`(Rdf.iri(adminDataNamedGraph.value)) + .insert(toTriples(group)) + + Update(deleteWhere(Rdf.iri(group.id.value), Vocabulary.KnoraAdmin.UserGroup, query, deletionFields)) + } + + private val deletionFields: List[Iri] = List( + Vocabulary.KnoraAdmin.groupName, + Vocabulary.KnoraAdmin.groupDescriptions, + Vocabulary.KnoraAdmin.status, + Vocabulary.KnoraAdmin.belongsToProject, + Vocabulary.KnoraAdmin.hasSelfJoinEnabled + ) + + private def toTriples(group: KnoraUserGroup) = { + import Vocabulary.KnoraAdmin.* + Rdf + .iri(group.id.value) + .has(RDF.TYPE, UserGroup) + .andHas(groupName, Rdf.literalOf(group.groupName.value)) + .andHas(groupDescriptions, group.groupDescriptions.toRdfLiterals: _*) + .andHas(status, Rdf.literalOf(group.status.value)) + .andHas(belongsToProject, group.belongsToProject.map(p => Rdf.iri(p.value)).toList: _*) + .andHas(hasSelfJoinEnabled, Rdf.literalOf(group.hasSelfJoinEnabled.value)) + } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala index 4304547a4d..9d1a5f02a2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala @@ -37,7 +37,6 @@ import org.knora.webapi.slice.admin.domain.service.KnoraUserRepo import org.knora.webapi.slice.admin.repo.rdf.RdfConversions.* import org.knora.webapi.slice.admin.repo.rdf.Vocabulary import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive.UserQueries -import org.knora.webapi.slice.common.repo.rdf.Errors.ConversionError import org.knora.webapi.slice.common.repo.rdf.RdfResource import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -71,13 +70,6 @@ final case class KnoraUserRepoLive(triplestore: TriplestoreService, cacheService } yield user private def toUser(resource: RdfResource) = { - def getObjectIrisConvert[A](r: RdfResource, prop: String)(implicit f: String => Either[String, A]) = for { - iris <- r.getObjectIris(prop) - as <- ZIO.foreach(iris)(it => - ZIO.fromEither(f(it.value)).mapError(err => ConversionError(s"Unable to parse $it: $err")) - ) - } yield as - for { userIri <- resource.iri.flatMap(it => ZIO.fromEither(UserIri.from(it.value))).mapError(TriplestoreResponseException.apply) @@ -88,10 +80,10 @@ final case class KnoraUserRepoLive(triplestore: TriplestoreService, cacheService passwordHash <- resource.getStringLiteralOrFail[PasswordHash](KnoraAdmin.Password) preferredLanguage <- resource.getStringLiteralOrFail[LanguageCode](KnoraAdmin.PreferredLanguage) status <- resource.getBooleanLiteralOrFail[UserStatus](KnoraAdmin.StatusProp) - isInProjectIris <- getObjectIrisConvert[ProjectIri](resource, KnoraAdmin.IsInProject) - isInGroupIris <- getObjectIrisConvert[GroupIri](resource, KnoraAdmin.IsInGroup) + isInProjectIris <- resource.getObjectIrisConvert[ProjectIri](KnoraAdmin.IsInProject) + isInGroupIris <- resource.getObjectIrisConvert[GroupIri](KnoraAdmin.IsInGroup) isInSystemAdminGroup <- resource.getBooleanLiteralOrFail[SystemAdmin](KnoraAdmin.IsInSystemAdminGroup) - isInProjectAdminGroupIris <- getObjectIrisConvert[ProjectIri](resource, KnoraAdmin.IsInProjectAdminGroup) + isInProjectAdminGroupIris <- resource.getObjectIrisConvert[ProjectIri](KnoraAdmin.IsInProjectAdminGroup) } yield KnoraUser( userIri, username, @@ -177,73 +169,61 @@ object KnoraUserRepoLive { Construct(query) } + private def deleteWhere( + id: Iri, + rdfType: Iri, + query: ModifyQuery, + iris: List[Iri] + ): ModifyQuery = + query + .delete(iris.zipWithIndex.foldLeft(id.has(RDF.TYPE, rdfType)) { case (p, (iri, index)) => + p.andHas(iri, variable(s"n${index}")) + }) + .where(iris.zipWithIndex.foldLeft(id.has(RDF.TYPE, rdfType).optional()) { case (p, (iri, index)) => + p.and(id.has(iri, variable(s"n${index}")).optional()) + }) + def save(u: KnoraUser): Update = { - val userIri = Rdf.iri(u.id.value) - val deletePattern = userIri - .has(RDF.TYPE, Vocabulary.KnoraAdmin.User) - .andHas(Vocabulary.KnoraAdmin.username, variable("previousUsername")) - .andHas(Vocabulary.KnoraAdmin.email, variable("previousEmail")) - .andHas(Vocabulary.KnoraAdmin.givenName, variable("previousGivenName")) - .andHas(Vocabulary.KnoraAdmin.familyName, variable("previousFamilyName")) - .andHas(Vocabulary.KnoraAdmin.status, variable("previousStatus")) - .andHas(Vocabulary.KnoraAdmin.preferredLanguage, variable("previousPreferredLanguage")) - .andHas(Vocabulary.KnoraAdmin.password, variable("previousPassword")) - .andHas(Vocabulary.KnoraAdmin.isInSystemAdminGroup, variable("previousIsInSystemAdminGroup")) - .andHas(Vocabulary.KnoraAdmin.isInProject, variable("previousIsInProject")) - .andHas(Vocabulary.KnoraAdmin.isInGroup, variable("previousIsInGroup")) - .andHas(Vocabulary.KnoraAdmin.isInProjectAdminGroup, variable("previousIsInProjectAdminGroup")) - val wherePattern = - userIri - .has(RDF.TYPE, Vocabulary.KnoraAdmin.User) - .optional() - .and(userIri.has(Vocabulary.KnoraAdmin.username, variable("previousUsername")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.email, variable("previousEmail")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.givenName, variable("previousGivenName")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.familyName, variable("previousFamilyName")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.status, variable("previousStatus")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.preferredLanguage, variable("previousPreferredLanguage")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.password, variable("previousPassword")).optional()) - .and( - userIri.has(Vocabulary.KnoraAdmin.isInSystemAdminGroup, variable("previousIsInSystemAdminGroup")).optional() - ) - .and(userIri.has(Vocabulary.KnoraAdmin.isInProject, variable("previousIsInProject")).optional()) - .and(userIri.has(Vocabulary.KnoraAdmin.isInGroup, variable("previousIsInGroup")).optional()) - .and( - userIri - .has(Vocabulary.KnoraAdmin.isInProjectAdminGroup, variable("previousIsInProjectAdminGroup")) - .optional() - ) val query: ModifyQuery = Queries .MODIFY() .prefix(prefix(RDF.NS), prefix(Vocabulary.KnoraAdmin.NS), prefix(XSD.NS)) .`with`(Rdf.iri(adminDataNamedGraph.value)) .insert(toTriples(u)) - .delete(deletePattern) - .where(wherePattern) - Update(query) + + Update(deleteWhere(Rdf.iri(u.id.value), Vocabulary.KnoraAdmin.User, query, deletionFields)) } + private val deletionFields: List[Iri] = List( + Vocabulary.KnoraAdmin.username, + Vocabulary.KnoraAdmin.email, + Vocabulary.KnoraAdmin.givenName, + Vocabulary.KnoraAdmin.familyName, + Vocabulary.KnoraAdmin.status, + Vocabulary.KnoraAdmin.preferredLanguage, + Vocabulary.KnoraAdmin.password, + Vocabulary.KnoraAdmin.isInSystemAdminGroup, + Vocabulary.KnoraAdmin.isInProject, + Vocabulary.KnoraAdmin.isInGroup, + Vocabulary.KnoraAdmin.isInProjectAdminGroup + ) + private def toTriples(u: KnoraUser) = { - val triples = - Rdf - .iri(u.id.value) - .has(RDF.TYPE, Vocabulary.KnoraAdmin.User) - .andHas(Vocabulary.KnoraAdmin.username, Rdf.literalOf(u.username.value)) - .andHas(Vocabulary.KnoraAdmin.email, Rdf.literalOf(u.email.value)) - .andHas(Vocabulary.KnoraAdmin.givenName, Rdf.literalOf(u.givenName.value)) - .andHas(Vocabulary.KnoraAdmin.familyName, Rdf.literalOf(u.familyName.value)) - .andHas(Vocabulary.KnoraAdmin.preferredLanguage, Rdf.literalOf(u.preferredLanguage.value)) - .andHas(Vocabulary.KnoraAdmin.status, Rdf.literalOf(u.status.value)) - .andHas(Vocabulary.KnoraAdmin.password, Rdf.literalOf(u.password.value)) - .andHas(Vocabulary.KnoraAdmin.isInSystemAdminGroup, Rdf.literalOf(u.isInSystemAdminGroup.value)) - - u.isInProject.foreach(project => triples.andHas(Vocabulary.KnoraAdmin.isInProject, Rdf.iri(project.value))) - u.isInGroup.foreach(group => triples.andHas(Vocabulary.KnoraAdmin.isInGroup, Rdf.iri(group.value))) - u.isInProjectAdminGroup.foreach(projectAdminGroup => - triples.andHas(Vocabulary.KnoraAdmin.isInProjectAdminGroup, Rdf.iri(projectAdminGroup.value)) - ) - triples + import Vocabulary.KnoraAdmin._ + Rdf + .iri(u.id.value) + .has(RDF.TYPE, User) + .andHas(username, Rdf.literalOf(u.username.value)) + .andHas(email, Rdf.literalOf(u.email.value)) + .andHas(givenName, Rdf.literalOf(u.givenName.value)) + .andHas(familyName, Rdf.literalOf(u.familyName.value)) + .andHas(preferredLanguage, Rdf.literalOf(u.preferredLanguage.value)) + .andHas(status, Rdf.literalOf(u.status.value)) + .andHas(password, Rdf.literalOf(u.password.value)) + .andHas(isInSystemAdminGroup, Rdf.literalOf(u.isInSystemAdminGroup.value)) + .andHas(isInProject, u.isInProject.map(p => Rdf.iri(p.value)).toList: _*) + .andHas(isInGroup, u.isInGroup.map(p => Rdf.iri(p.value)).toList: _*) + .andHas(isInProjectAdminGroup, u.isInProjectAdminGroup.map(p => Rdf.iri(p.value)).toList: _*) } } val layer = ZLayer.derive[KnoraUserRepoLive] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/AuthorizationRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/AuthorizationRestService.scala index 379a70599e..5d5eea201b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/AuthorizationRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/AuthorizationRestService.scala @@ -97,31 +97,6 @@ final case class AuthorizationRestServiceLive(projectRepo: KnoraProjectRepo, gro s"You are logged in with username '${user.username}', but only a system administrator has permissions for this operation." checkActiveUser(user, isSystemAdmin, msg) } - - private def checkActiveUser( - user: User, - condition: User => Boolean, - errorMsg: String - ): IO[ForbiddenException, Unit] = - ensureIsActive(user) *> ZIO.fail(ForbiddenException(errorMsg)).unless(condition(user)).unit - - private def ensureIsActive(user: User): IO[ForbiddenException, Unit] = { - lazy val msg = s"The account with username '${user.username}' is not active." - ZIO.fail(ForbiddenException(msg)).unless(isActive(user)).unit - } - - override def ensureSystemAdminOrProjectAdmin( - user: User, - projectIri: ProjectIri - ): IO[ForbiddenException, KnoraProject] = - for { - project <- projectRepo - .findById(projectIri) - .orDie - .someOrFail(ForbiddenException(s"Project with IRI '${projectIri.value}' not found")) - _ <- ensureSystemAdminOrProjectAdmin(user, project) - } yield project - override def ensureSystemAdminOrProjectAdminOfGroup( user: User, groupIri: GroupIri @@ -131,9 +106,22 @@ final case class AuthorizationRestServiceLive(projectRepo: KnoraProjectRepo, gro .findById(groupIri) .orDie .someOrFail(ForbiddenException(s"Group with IRI '${groupIri.value}' not found")) - project <- ensureSystemAdminOrProjectAdmin(user, group.belongsToProject) + projectIri <- ZIO + .succeed(group.belongsToProject) + .someOrFail(ForbiddenException(s"Group with IRI '${groupIri.value}' not found")) + project <- ensureSystemAdminOrProjectAdmin(user, projectIri) } yield project + override def ensureSystemAdminOrProjectAdmin( + user: User, + projectIri: ProjectIri + ): IO[ForbiddenException, KnoraProject] = + projectRepo + .findById(projectIri) + .orDie + .someOrFail(ForbiddenException(s"Project with IRI '${projectIri.value}' not found")) + .tap(ensureSystemAdminOrProjectAdmin(user, _)) + override def ensureSystemAdminOrProjectAdmin(user: User, project: KnoraProject): IO[ForbiddenException, Unit] = { lazy val msg = s"You are logged in with username '${user.username}', but only a system administrator or project administrator has permissions for this operation." @@ -157,6 +145,18 @@ final case class AuthorizationRestServiceLive(projectRepo: KnoraProjectRepo, gro val msg = "ProjectAdmin or SystemAdmin permissions are required." checkActiveUser(user, isSystemAdminSystemUserOrProjectAdminInAnyProject, msg) } + + private def checkActiveUser( + user: User, + condition: User => Boolean, + errorMsg: String + ): IO[ForbiddenException, Unit] = + ensureIsActive(user) *> ZIO.fail(ForbiddenException(errorMsg)).unless(condition(user)).unit + + private def ensureIsActive(user: User): IO[ForbiddenException, Unit] = { + lazy val msg = s"The account with username '${user.username}' is not active." + ZIO.fail(ForbiddenException(msg)).unless(isActive(user)).unit + } } object AuthorizationRestServiceLive { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala index f288a701b4..382ef2b6f5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala @@ -341,6 +341,13 @@ final case class RdfResource(private val res: Resource) { def getObjectIris(propertyIri: String): IO[RdfError, Chunk[InternalIri]] = getObjectUris(propertyIri).map(_.map(InternalIri)) + def getObjectIrisConvert[A](prop: String)(implicit map: String => Either[String, A]) = + getObjectIris(prop).flatMap { iris => + ZIO.foreach(iris) { iri => + ZIO.fromEither(map(iri.value)).mapError(err => ConversionError(s"Unable to parse $iri: $err")) + } + } + /** * Returns the IRIs of the objects of a given predicate IRI. * Fails if the objects are not present. diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index ff73c2f1c0..0311168a25 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -23,6 +23,7 @@ import org.knora.webapi.slice.admin.domain.model.SystemAdmin import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.UserStatus import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.admin.domain.model._ /** * Helps in creating value objects for tests. @@ -63,6 +64,17 @@ object TestDataFactory { ) } + object UserGroup { + val testUserGroup: KnoraUserGroup = KnoraUserGroup( + GroupIri.unsafeFrom("http://rdfh.ch/groups/0001/1234"), + GroupName.unsafeFrom("User Group"), + GroupDescriptions.unsafeFrom(List(V2.StringLiteralV2("one user group to rule them all", None))), + GroupStatus.from(true), + Some(ProjectIri.unsafeFrom("http://rdfh.ch/projects/0001")), + GroupSelfJoin.from(false) + ) + } + val someProject = KnoraProject( ProjectIri.unsafeFrom("http://rdfh.ch/projects/0001"), Shortname.unsafeFrom("shortname"), diff --git a/webapi/src/test/scala/dsp/valueobjects/GroupSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroupSpec.scala similarity index 81% rename from webapi/src/test/scala/dsp/valueobjects/GroupSpec.scala rename to webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroupSpec.scala index 8a135ef5c4..98712c345d 100644 --- a/webapi/src/test/scala/dsp/valueobjects/GroupSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraUserGroupSpec.scala @@ -7,15 +7,15 @@ package dsp.valueobjects import zio.test.* -import dsp.valueobjects.Group.* +import org.knora.webapi.slice.admin.domain.model.* /** * This spec is used to test the [[Group]] value objects creation. */ -object GroupSpec extends ZIOSpecDefault { +object KnoraUserGroupSpec extends ZIOSpecDefault { private val validDescription = Seq(V2.StringLiteralV2(value = "Valid group description", language = Some("en"))) private val invalidDescription = Seq( - V2.StringLiteralV2(value = "Invalid group description \r", language = Some("en")) + V2.StringLiteralV2(value = "", language = Some("en")) ) def spec: Spec[Any, Any] = groupNameTest + groupDescriptionsTest + groupStatusTest + groupSelfJoinTest @@ -24,9 +24,6 @@ object GroupSpec extends ZIOSpecDefault { test("not be created from an empty value") { assertTrue(GroupName.from("") == Left(GroupErrorMessages.GroupNameMissing)) }, - test("not be created from an invalid value") { - assertTrue(GroupName.from("Invalid group name\r") == Left(GroupErrorMessages.GroupNameInvalid)) - }, test("be created from a valid value") { assertTrue(GroupName.from("Valid group name").map(_.value) == Right("Valid group name")) } @@ -57,8 +54,8 @@ object GroupSpec extends ZIOSpecDefault { private val groupSelfJoinTest = suite("GroupSelfJoin")( test("should be created from a valid value") { assertTrue( - GroupSelfJoin.from(true) == GroupSelfJoin.possible, - GroupSelfJoin.from(false) == GroupSelfJoin.impossible + GroupSelfJoin.from(true) == GroupSelfJoin.enabled, + GroupSelfJoin.from(false) == GroupSelfJoin.disabled ) } ) diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLiveSpec.scala index 1d79c89c9c..983a6c158f 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserGroupRepoLiveSpec.scala @@ -6,12 +6,20 @@ package org.knora.webapi.slice.admin.repo.service import zio.Ref +import zio.ZIO import zio.ZLayer +import zio.test.Spec +import zio.test.ZIOSpecDefault +import zio.test.assertTrue +import org.knora.webapi.TestDataFactory.UserGroup.* +import org.knora.webapi.messages.StringFormatter import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupName import org.knora.webapi.slice.admin.domain.model.KnoraUserGroup import org.knora.webapi.slice.admin.domain.service.KnoraUserGroupRepo import org.knora.webapi.slice.common.repo.AbstractInMemoryCrudRepository +import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory final case class KnoraUserGroupRepoInMemory(groups: Ref[List[KnoraUserGroup]]) extends AbstractInMemoryCrudRepository[KnoraUserGroup, GroupIri](groups, _.id) @@ -21,3 +29,43 @@ object KnoraUserGroupRepoInMemory { val layer = ZLayer.fromZIO(Ref.make(List.empty[KnoraUserGroup])) >>> ZLayer.derive[KnoraUserGroupRepoInMemory] } + +object KnoraUserGroupRepoLiveSpec extends ZIOSpecDefault { + val spec: Spec[Any, Any] = suite("KnoraUserGroupRepoLive")( + suite("findById")( + test("findById given a non existing user should return None") { + ZIO.serviceWithZIO[KnoraUserGroupRepo](userGroupRepo => + for { + userGroup <- userGroupRepo.findById(GroupIri.unsafeFrom("http://rdfh.ch/groups/0001/1234")) + } yield assertTrue(userGroup.isEmpty) + ) + }, + test("findById given an existing user should return that user") { + ZIO.serviceWithZIO[KnoraUserGroupRepo](userGroupRepo => + for { + _ <- userGroupRepo.save(testUserGroup) + userGroup <- userGroupRepo.findById(testUserGroup.id) + } yield assertTrue(userGroup.contains(testUserGroup)) + ) + }, + test("save should update fields") { + ZIO.serviceWithZIO[KnoraUserGroupRepo](userGroupRepo => + for { + _ <- userGroupRepo.save(testUserGroup) + testUserGroupModified = + testUserGroup.copy( + groupName = GroupName.unsafeFrom("another"), + belongsToProject = None + ) + _ <- userGroupRepo.save(testUserGroupModified) + userGroup <- userGroupRepo.findById(testUserGroup.id) + } yield assertTrue(userGroup.contains(testUserGroupModified)) + ) + } + ) + ).provide( + KnoraUserGroupRepoLive.layer, + TriplestoreServiceInMemory.emptyLayer, + StringFormatter.test + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala index 697a04b813..f3fd46344d 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala @@ -75,7 +75,7 @@ object KnoraUserRepoLiveSpec extends ZIOSpecDefault { val spec: Spec[Any, Any] = suite("UserRepoLiveSpec")( suite("findById")( - test("findById given an existing user should return that user") { + test("findById given an existing user should return that user") { for { _ <- storeUsersInTripleStore(testUser) user <- findById(testUser.id) @@ -158,7 +158,7 @@ object KnoraUserRepoLiveSpec extends ZIOSpecDefault { updatedUser <- findById(testUserWithoutAnyGroups.id).someOrFail(new Exception("User not found")) } yield assertTrue(updatedUser.isInProject.isEmpty) }, - test("should update an existing user isInProjectAdminGroup ") { + test("should update an existing user isInProjectAdminGroup") { for { _ <- save(testUserWithoutAnyGroups) // create the user _ <- save(testUserWithoutAnyGroups.copy(isInProjectAdminGroup = Chunk(TestDataFactory.someProject.id))) diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceInMemory.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceInMemory.scala index 8ed52640dd..88c00c890d 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceInMemory.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceInMemory.scala @@ -5,6 +5,7 @@ package org.knora.webapi.store.triplestore.api import org.apache.jena.query.* +import org.apache.jena.rdf.model import org.apache.jena.rdf.model.Model import org.apache.jena.rdf.model.ModelFactory import org.apache.jena.riot.RDFDataMgr @@ -28,8 +29,8 @@ import java.io.InputStream import java.nio.charset.StandardCharsets import java.nio.file.Path import java.nio.file.Paths -import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.jdk.CollectionConverters.IteratorHasAsScala +import scala.jdk.CollectionConverters._ import org.knora.webapi.IRI import org.knora.webapi.messages.StringFormatter @@ -59,6 +60,7 @@ trait TestTripleStore extends TriplestoreService { def setDataset(ds: Dataset): UIO[Unit] def getDataset: UIO[Dataset] def printDataset: UIO[Unit] + def datasetStatements: RIO[Scope, List[model.Statement]] } final case class TriplestoreServiceInMemory(datasetRef: Ref[Dataset], implicit val sf: StringFormatter) @@ -255,6 +257,9 @@ final case class TriplestoreServiceInMemory(datasetRef: Ref[Dataset], implicit v _ = printDatasetContents(ds) } yield () + override def datasetStatements: RIO[Scope, List[model.Statement]] = + getDataSetWithTransaction(ReadWrite.READ).map(_.getUnionModel.listStatements.toList.asScala.toList) + def printDatasetContents(dataset: Dataset): Unit = { // Iterate over the named models dataset.begin(ReadWrite.READ)