Skip to content

Commit

Permalink
Introduce GroupIri PathVariable and move GroupIri to slice admin package
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone committed Dec 28, 2023
1 parent 52b2d6a commit 28fe092
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,24 @@

package org.knora.webapi.responders.admin

import org.apache.pekko
import org.apache.pekko.actor.Status.Failure

import java.util.UUID

import dsp.errors.BadRequestException
import dsp.errors.DuplicateValueException
import dsp.errors.NotFoundException
import dsp.valueobjects.Group._
import dsp.valueobjects.Iri._
import dsp.valueobjects.V2
import org.knora.webapi._
import org.knora.webapi.messages.admin.responder.groupsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.UserInformationTypeADM
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.sharedtestdata.SharedTestDataADM
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.util.MutableTestIri

import pekko.actor.Status.Failure

/**
* This spec is used to test the messages received by the [[org.knora.webapi.responders.admin.GroupsResponderADMSpec]] actor.
*/
Expand Down Expand Up @@ -107,9 +105,7 @@ class GroupsResponderADMSpec extends CoreSpec {
"return a 'DuplicateValueException' if the supplied group name is not unique" in {
appActor ! GroupCreateRequestADM(
createRequest = GroupCreatePayloadADM(
id = Some(
GroupIri.make(imagesReviewerGroup.id).fold(e => throw e.head, v => v)
),
id = Some(GroupIri.unsafeFrom(imagesReviewerGroup.id)),
name = GroupName.make("NewGroup").fold(e => throw e.head, v => v),
descriptions = GroupDescriptions
.make(Seq(V2.StringLiteralV2(value = "NewGroupDescription", language = Some("en"))))
Expand Down
33 changes: 0 additions & 33 deletions webapi/src/main/scala/dsp/valueobjects/Iri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,6 @@ object Iri {
def isUserIri(iri: IRI): Boolean =
isIri(iri) && iri.startsWith("http://rdfh.ch/users/")

/**
* Returns `true` if an IRI string looks like a Knora group IRI.
*
* @param iri the IRI to be checked.
*/
def isGroupIri(iri: IRI): Boolean =
isIri(iri) && iri.startsWith("http://rdfh.ch/groups/")

/**
* Returns `true` if an IRI string looks like a Knora project IRI
*
Expand Down Expand Up @@ -187,29 +179,6 @@ object Iri {
/**
* GroupIri value object.
*/
sealed abstract case class GroupIri private (value: String) extends Iri
object GroupIri { self =>
def make(value: String): Validation[Throwable, GroupIri] =
if (value.isEmpty) Validation.fail(BadRequestException(IriErrorMessages.GroupIriMissing))
else {
val isUuid: Boolean = UuidUtil.hasValidLength(value.split("/").last)

if (!isGroupIri(value))
Validation.fail(BadRequestException(IriErrorMessages.GroupIriInvalid))
else if (isUuid && !UuidUtil.hasSupportedVersion(value))
Validation.fail(BadRequestException(IriErrorMessages.UuidVersionInvalid))
else
validateAndEscapeIri(value)
.mapError(_ => BadRequestException(IriErrorMessages.GroupIriInvalid))
.map(new GroupIri(_) {})
}

def make(value: Option[String]): Validation[Throwable, Option[GroupIri]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* ListIri value object.
Expand Down Expand Up @@ -326,8 +295,6 @@ object Iri {
}

object IriErrorMessages {
val GroupIriMissing = "Group IRI cannot be empty."
val GroupIriInvalid = "Group IRI is invalid."
val ListIriMissing = "List IRI cannot be empty."
val ListIriInvalid = "List IRI is invalid"
val ProjectIriMissing = "Project IRI cannot be empty."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package org.knora.webapi.messages

import spray.json.*
import zio.ZLayer
import zio.prelude.Validation

import java.time.*
import java.time.temporal.ChronoField
Expand Down Expand Up @@ -624,9 +623,6 @@ class StringFormatter private (
private val ProjectIDPattern: String =
"""\p{XDigit}{4,4}"""

// A regex for matching a string containing the project ID.
private val ProjectIDRegex: Regex = ("^" + ProjectIDPattern + "$").r

// A regex for the URL path of an API v2 ontology (built-in or project-specific).
private val ApiV2OntologyUrlPathRegex: Regex = (
"^" + "/ontology/((" +
Expand Down Expand Up @@ -1525,16 +1521,6 @@ class StringFormatter private (
case _ => false
}

/**
* Given the group IRI, checks if it is in a valid format.
*
* @param iri the group's IRI.
* @return the IRI of the list.
*/
def validateGroupIri(iri: IRI): Validation[ValidationException, IRI] =
if (Iri.isGroupIri(iri)) Validation.succeed(iri)
else Validation.fail(ValidationException(s"Invalid IRI: $iri"))

/**
* Given the permission IRI, checks if it is in a valid format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package org.knora.webapi.messages.admin.responder.groupsmessages

import dsp.valueobjects.Group.*
import dsp.valueobjects.Iri.*
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsADMJso
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol
import org.knora.webapi.messages.traits.Jsonable
import org.knora.webapi.slice.admin.domain.model.GroupIri

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// API requests
Expand Down Expand Up @@ -55,7 +56,7 @@ case class CreateAdministrativePermissionAPIRequestADM(
if (hasPermissions.isEmpty) throw BadRequestException("Permissions needs to be supplied.")

if (!OntologyConstants.KnoraAdmin.BuiltInGroups.contains(forGroup)) {
sf.validateGroupIri(forGroup).getOrElse(throw BadRequestException(s"Invalid group IRI $forGroup"))
GroupIri.from(forGroup).getOrElse(throw BadRequestException(s"Invalid group IRI $forGroup"))

Check warning on line 59 in webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala#L59

Added line #L59 was not covered by tests
}

def prepareHasPermissions: CreateAdministrativePermissionAPIRequestADM =
Expand Down Expand Up @@ -101,8 +102,7 @@ case class CreateDefaultObjectAccessPermissionAPIRequestADM(
throw BadRequestException("Not allowed to supply groupIri and propertyIri together.")
else {
if (!OntologyConstants.KnoraAdmin.BuiltInGroups.contains(iri)) {
sf.validateGroupIri(iri)
.getOrElse(throw BadRequestException(s"Invalid group IRI ${forGroup.get}"))
GroupIri.from(iri).getOrElse(throw BadRequestException(s"Invalid group IRI ${forGroup.get}"))

Check warning on line 105 in webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala#L105

Added line #L105 was not covered by tests

Check warning on line 105 in webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala#L105

Usage of get on optional type.
}
}
case None =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

package org.knora.webapi.routing.admin

import org.apache.pekko
import org.apache.pekko.http.scaladsl.server.Directives.*
import org.apache.pekko.http.scaladsl.server.PathMatcher
import org.apache.pekko.http.scaladsl.server.Route
import zio.*
import zio.prelude.Validation

import dsp.errors.BadRequestException
import dsp.valueobjects.Group.*
import dsp.valueobjects.Iri
import dsp.valueobjects.Iri.*
import org.knora.webapi.core.MessageRelay
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.groupsmessages.*
Expand All @@ -21,12 +22,9 @@ import org.knora.webapi.routing.KnoraRoute
import org.knora.webapi.routing.KnoraRouteData
import org.knora.webapi.routing.RouteUtilADM.*
import org.knora.webapi.routing.RouteUtilZ
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri

import pekko.http.scaladsl.server.Directives.*
import pekko.http.scaladsl.server.PathMatcher
import pekko.http.scaladsl.server.Route

/**
* Provides a routing function for API routes that deal with groups.
*/
Expand Down Expand Up @@ -76,7 +74,9 @@ final case class GroupsRouteADM(
private def createGroup(): Route = path(groupsBasePath) {
post {
entity(as[CreateGroupApiRequestADM]) { apiRequest => requestContext =>
val id: Validation[Throwable, Option[GroupIri]] = GroupIri.make(apiRequest.id)
val id: Validation[Throwable, Option[GroupIri]] = apiRequest.id
.map(id => Validation.fromEither(GroupIri.from(id).map(Some(_))).mapError(BadRequestException(_)))
.getOrElse(Validation.succeed(None))

Check warning on line 79 in webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala#L78-L79

Added lines #L78 - L79 were not covered by tests
val name: Validation[Throwable, GroupName] = GroupName.make(apiRequest.name)
val descriptions: Validation[Throwable, GroupDescriptions] = GroupDescriptions.make(apiRequest.descriptions)
val project: Validation[Throwable, ProjectIri] = ProjectIri.from(apiRequest.project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ import dsp.errors.BadRequestException
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortnameIdentifier
import org.knora.webapi.slice.admin.domain.model.GroupIri

object AdminPathVariables {

val groupIri: EndpointInput.PathCapture[GroupIri] =
path[GroupIri]
.name("groupIri")
.description("The IRI of a group. Must be URL-encoded.")
.example(GroupIri.unsafeFrom("http://rdfh.ch/groups/0001/qCSZzdAJCBqw_2snW5Q7NC"))

Check warning on line 23 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala#L20-L23

Added lines #L20 - L23 were not covered by tests

val projectIri: EndpointInput.PathCapture[IriIdentifier] =
path[IriIdentifier]
.name("projectIri")
.description("The IRI of a project. Must be URL-encoded.")
.example(IriIdentifier.fromString("http://rdfh.ch/projects/0001").fold(e => throw e.head, identity))
.example(IriIdentifier.unsafeFrom("http://rdfh.ch/projects/0001"))

Check warning on line 29 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala#L29

Added line #L29 was not covered by tests

private val projectShortcodeCodec: Codec[String, ShortcodeIdentifier, TextPlain] =
Codec.string.mapDecode(str =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.Administrat
import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionsForProjectGetResponseADM
import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateAdministrativePermissionAPIRequestADM
import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsADMJsonProtocol
import org.knora.webapi.slice.admin.api.AdminPathVariables.groupIri
import org.knora.webapi.slice.admin.api.AdminPathVariables.projectIri
import org.knora.webapi.slice.common.api.BaseEndpoints

Expand All @@ -33,10 +34,8 @@ final case class PermissionsEndpoints(base: BaseEndpoints) extends PermissionsAD
.description("Get all administrative permissions for a project.")
.out(sprayJsonBody[AdministrativePermissionsForProjectGetResponseADM])

Check warning on line 35 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala#L33-L35

Added lines #L33 - L35 were not covered by tests

private val groupIriPathVar = path[String].name("groupIri").description("The IRI of the group")

val getPermissionsApByProjectAndGroupIri = base.securedEndpoint.get
.in(permissionsBase / "ap" / projectIri / groupIriPathVar)
.in(permissionsBase / "ap" / projectIri / groupIri)
.description("Get all administrative permissions for a project and a group.")
.out(sprayJsonBody[AdministrativePermissionGetResponseADM])

Check warning on line 40 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala#L38-L40

Added lines #L38 - L40 were not covered by tests

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.Administrat
import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateAdministrativePermissionAPIRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier
import org.knora.webapi.slice.admin.api.service.PermissionsRestService
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.common.api.HandlerMapper
import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler

Expand Down Expand Up @@ -46,11 +47,11 @@ final case class PermissionsEndpointsHandlers(

private val getPermissionsApByProjectAndGroupIriHandler =
SecuredEndpointAndZioHandler[
(IriIdentifier, String),
(IriIdentifier, GroupIri),
AdministrativePermissionGetResponseADM
](
permissionsEndpoints.getPermissionsApByProjectAndGroupIri,

Check warning on line 53 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala#L52-L53

Added lines #L52 - L53 were not covered by tests
user => { case (projectIri: IriIdentifier, groupIri: String) =>
user => { case (projectIri: IriIdentifier, groupIri: GroupIri) =>
restService.getPermissionsApByProjectAndGroupIri(projectIri.value, groupIri, user)

Check warning on line 55 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala#L55

Added line #L55 was not covered by tests
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.Administrat
import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateAdministrativePermissionAPIRequestADM
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.responders.admin.PermissionsResponderADM
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.service.KnoraProjectRepo
import org.knora.webapi.slice.common.api.AuthorizationRestService
Expand Down Expand Up @@ -59,11 +60,11 @@ final case class PermissionsRestService(

def getPermissionsApByProjectAndGroupIri(
projectIri: KnoraProject.ProjectIri,
groupIri: String,
groupIri: GroupIri,
user: UserADM
): Task[AdministrativePermissionGetResponseADM] = for {
_ <- ensureProjectIriExistsAndUserHasAccess(projectIri, user)
result <- responder.getPermissionsApByProjectAndGroupIri(projectIri.value, groupIri)
result <- responder.getPermissionsApByProjectAndGroupIri(projectIri.value, groupIri.value)

Check warning on line 67 in webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionsRestService.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/PermissionsRestService.scala#L66-L67

Added lines #L66 - L67 were not covered by tests
} yield result
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.knora.webapi.slice.admin.domain.model

import sttp.tapir.Codec
import sttp.tapir.CodecFormat

import dsp.valueobjects.Iri
import dsp.valueobjects.Iri.validateAndEscapeIri
import dsp.valueobjects.UuidUtil

final case class GroupIri private (value: String) extends Iri

object GroupIri {

implicit val tapirCodec: Codec[String, GroupIri, CodecFormat.TextPlain] =
Codec.string.mapEither(GroupIri.from)(_.value)

def unsafeFrom(value: String): GroupIri = from(value).fold(e => throw new IllegalArgumentException(e), identity)

Check warning on line 17 in webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala#L17

Added line #L17 was not covered by tests

def from(value: String): Either[String, GroupIri] =
if (value.isEmpty) Left("Group IRI cannot be empty.")

Check notice on line 20 in webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala#L20

Consider using case matching instead of else if blocks
else if (!(Iri.isIri(value) && value.startsWith("http://rdfh.ch/groups/")))

Check notice on line 21 in webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/GroupIri.scala#L21

Consider using case matching instead of else if blocks
Left("Group IRI is invalid.")
else if (UuidUtil.hasValidLength(value.split("/").last) && !UuidUtil.hasSupportedVersion(value))
Left("Invalid UUID used to create IRI. Only versions 4 and 5 are supported.")
else
validateAndEscapeIri(value).toEither
.map(GroupIri.apply)
.left
.map(_ => "Group IRI is invalid.")
}
42 changes: 1 addition & 41 deletions webapi/src/test/scala/dsp/valueobjects/IriSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,47 +43,7 @@ object IriSpec extends ZIOSpecDefault {
val uuidVersion3 = fromIri(userIriWithUUIDVersion3)
val supportedUuid = fromIri(validUserIri)

def spec: Spec[Any, Throwable] = groupIriTest + listIriTest + uuidTest + roleIriTest + userIriTest

private val groupIriTest = suite("IriSpec - GroupIri")(
test("pass an empty value and return an error") {
assertTrue(
GroupIri.make("") == Validation.fail(BadRequestException(IriErrorMessages.GroupIriMissing)),
GroupIri.make(Some("")) == Validation.fail(BadRequestException(IriErrorMessages.GroupIriMissing))
)
},
test("pass an invalid value and return an error") {
assertTrue(
GroupIri.make(invalidIri) == Validation.fail(BadRequestException(IriErrorMessages.GroupIriInvalid)),
GroupIri.make(Some(invalidIri)) == Validation.fail(BadRequestException(IriErrorMessages.GroupIriInvalid))
)
},
test("pass an invalid IRI containing unsupported UUID version and return an error") {
assertTrue(
GroupIri.make(groupIriWithUUIDVersion3) == Validation.fail(
BadRequestException(IriErrorMessages.UuidVersionInvalid)
),
GroupIri.make(Some(groupIriWithUUIDVersion3)) == Validation.fail(
BadRequestException(IriErrorMessages.UuidVersionInvalid)
)
)
},
test("pass a valid value and successfully create value object") {
val groupIri = GroupIri.make(validGroupIri)
val maybeGroupIri = GroupIri.make(Some(validGroupIri))

(for {
iri <- groupIri
maybeIri <- maybeGroupIri
} yield assertTrue(iri.value == validGroupIri) &&
assert(maybeIri)(isSome(equalTo(iri)))).toZIO
},
test("successfully validate passing None") {
assertTrue(
GroupIri.make(None) == Validation.succeed(None)
)
}
)
def spec: Spec[Any, Throwable] = listIriTest + uuidTest + roleIriTest + userIriTest

private val listIriTest = suite("IriSpec - ListIri")(
test("pass an empty value and return an error") {
Expand Down
Loading

0 comments on commit 28fe092

Please sign in to comment.