Skip to content
Permalink
Browse files
feat(UUID): add IRI validation that allows only to create IRIs using …
…UUID version 4 and 5 (DEV-402) (#1990)

* add method that checks if UUID version is correct

* improve UUID version checking implementation

* add UUID check to admin velue objects

* fix bad test IRIs

* add permission iri validation

* add resource iri validation

* fix permisson bad IRIs

* add value iri validation

* cleanup

* more cleanup

* refactor validate methods

* refactor UUID method

* fix typos

* fix failing tests
  • Loading branch information
mpro7 committed Feb 3, 2022
1 parent 65952f9 commit 74d43441e6960acd6e86ea2a67ebdf3f6fdf4125
Showing with 208 additions and 69 deletions.
  1. +48 −5 webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala
  2. +21 −17 .../scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala
  3. +5 −0 ...src/main/scala/org/knora/webapi/messages/admin/responder/valueObjects/GroupsValueObjectsADM.scala
  4. +6 −0 .../src/main/scala/org/knora/webapi/messages/admin/responder/valueObjects/ListsValueObjectsADM.scala
  5. +7 −1 ...c/main/scala/org/knora/webapi/messages/admin/responder/valueObjects/ProjectsValueObjectsADM.scala
  6. +6 −0 .../src/main/scala/org/knora/webapi/messages/admin/responder/valueObjects/UsersValueObjectsADM.scala
  7. +1 −1 webapi/src/main/scala/org/knora/webapi/messages/util/standoff/XMLToStandoffUtil.scala
  8. +9 −4 ...i/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala
  9. +8 −1 webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala
  10. +1 −1 webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala
  11. +1 −5 webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala
  12. +1 −8 webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala
  13. +2 −2 webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala
  14. +1 −3 webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala
  15. +1 −1 webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
  16. +2 −3 webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala
  17. +29 −8 webapi/src/test/scala/org/knora/webapi/messages/StringFormatterSpec.scala
  18. +37 −0 ...la/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADMSpec.scala
  19. +5 −0 ...test/scala/org/knora/webapi/messages/admin/responder/valueObjects/GroupsValueObjectsADMSpec.scala
  20. +3 −0 .../test/scala/org/knora/webapi/messages/admin/responder/valueObjects/ListsValueObjectsADMSpec.scala
  21. +5 −3 ...st/scala/org/knora/webapi/messages/admin/responder/valueObjects/ProjectsValueObjectsADMSpec.scala
  22. +3 −0 .../test/scala/org/knora/webapi/messages/admin/responder/valueObjects/UsersValueObjectsADMSpec.scala
  23. +3 −3 webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala
  24. +3 −3 webapi/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala
@@ -47,6 +47,7 @@ import scala.util.{Failure, Success, Try}
* Provides instances of [[StringFormatter]], as well as string formatting constants.
*/
object StringFormatter {
val UUID_INVALID_ERROR = "Invalid UUID used to create IRI. Only versions 4 and 5 are supported."

// A non-printing delimiter character, Unicode INFORMATION SEPARATOR ONE, that should never occur in data.
val INFORMATION_SEPARATOR_ONE = '\u001F'
@@ -898,7 +899,7 @@ class StringFormatter private (
* about the IRI being constructed.
* @param errorFun a function that throws an exception. It will be called if the IRI is invalid.
*/
private class SmartIriImpl(iriStr: IRI, parsedIriInfo: Option[SmartIriInfo], errorFun: => Nothing) extends SmartIri {
class SmartIriImpl(iriStr: IRI, parsedIriInfo: Option[SmartIriInfo], errorFun: => Nothing) extends SmartIri {
def this(iriStr: IRI) = this(iriStr, None, throw DataConversionException(s"Couldn't parse IRI: $iriStr"))

def this(iriStr: IRI, parsedIriInfo: Option[SmartIriInfo]) =
@@ -2961,14 +2962,56 @@ class StringFormatter private (
errorFun
}

/**
* Gets the last segment of IRI, decodes UUID and gets the version.
* @param s the string (IRI) to be checked.
* @return UUID version.
*/
def getUUIDVersion(s: IRI): Int = {
val encodedUUID = s.split("/").last
decodeUuid(encodedUUID).version()
}

/**
* Checks if UUID used to create IRI has correct version (4 and 5 are allowed).
* @param s the string (IRI) to be checked.
* @return TRUE for correct versions, FALSE for incorrect.
*/
def isUUIDVersion4Or5(s: IRI): Boolean =
if (getUUIDVersion(s) == 4 || getUUIDVersion(s) == 5) {
true
} else {
false
}

/**
* Checks if a string is the right length to be a canonical or Base64-encoded UUID.
*
* @param idStr the string to check.
* @return `true` if the string is the right length to be a canonical or Base64-encoded UUID.
* @param s the string to check.
* @return TRUE if the string is the right length to be a canonical or Base64-encoded UUID.
*/
def couldBeUuid(idStr: String): Boolean =
idStr.length == CanonicalUuidLength || idStr.length == Base64UuidLength
def hasUUIDLength(s: String): Boolean =
s.length == CanonicalUuidLength || s.length == Base64UuidLength

/**
* Validates resource IRI
* @param iri to be validated
*/
def validateUUIDOfResourceIRI(iri: SmartIri): Unit =
if (iri.isKnoraResourceIri && hasUUIDLength(iri.toString.split("/").last) && !isUUIDVersion4Or5(iri.toString)) {
throw BadRequestException(UUID_INVALID_ERROR)
}

/**
* Validates permission IRI
* @param iri to be validated.
*/
def validatePermissionIRI(iri: IRI): Unit =
if (isKnoraPermissionIriStr(iri) && !isUUIDVersion4Or5(iri)) {
throw BadRequestException(UUID_INVALID_ERROR)
} else {
validatePermissionIri(iri, throw BadRequestException(s"Invalid permission IRI ${iri} is given."))
}

/**
* Creates a new resource IRI based on a UUID.
@@ -9,7 +9,7 @@ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import org.knora.webapi._
import org.knora.webapi.exceptions.{BadRequestException, ForbiddenException}
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionProfileType.Restricted
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsADMJsonProtocol
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.admin.responder.{KnoraRequestADM, KnoraResponseADM}
@@ -37,19 +37,21 @@ case class CreateAdministrativePermissionAPIRequestADM(
forGroup: IRI,
hasPermissions: Set[PermissionADM]
) extends PermissionsADMJsonProtocol {
implicit protected val sf: StringFormatter = StringFormatter.getInstanceForConstantOntologies

id match {
case Some(iri) => sf.validatePermissionIRI(iri)
case None => None
}

def toJsValue: JsValue = createAdministrativePermissionAPIRequestADMFormat.write(this)

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))
stringFormatter.validateOptionalPermissionIri(
id,
throw BadRequestException(s"Invalid permission IRI ${id.get} is given.")
)
sf.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))

if (hasPermissions.isEmpty) throw BadRequestException("Permissions needs to be supplied.")

if (!OntologyConstants.KnoraAdmin.BuiltInGroups.contains(forGroup)) {
stringFormatter.validateGroupIri(forGroup, throw BadRequestException(s"Invalid group IRI $forGroup"))
sf.validateGroupIri(forGroup, throw BadRequestException(s"Invalid group IRI $forGroup"))
}

def prepareHasPermissions: CreateAdministrativePermissionAPIRequestADM =
@@ -76,14 +78,16 @@ case class CreateDefaultObjectAccessPermissionAPIRequestADM(
forProperty: Option[IRI] = None,
hasPermissions: Set[PermissionADM]
) extends PermissionsADMJsonProtocol {
implicit protected val sf: StringFormatter = StringFormatter.getInstanceForConstantOntologies

id match {
case Some(iri) => sf.validatePermissionIRI(iri)
case None => None
}

def toJsValue: JsValue = createDefaultObjectAccessPermissionAPIRequestADMFormat.write(this)

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))
stringFormatter.validateOptionalPermissionIri(
id,
throw BadRequestException(s"Invalid permission IRI ${id.get} is given.")
)
sf.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))

forGroup match {
case Some(iri: IRI) =>
@@ -93,7 +97,7 @@ case class CreateDefaultObjectAccessPermissionAPIRequestADM(
throw BadRequestException("Not allowed to supply groupIri and propertyIri together.")
else {
if (!OntologyConstants.KnoraAdmin.BuiltInGroups.contains(iri)) {
stringFormatter.validateOptionalGroupIri(
sf.validateOptionalGroupIri(
forGroup,
throw BadRequestException(s"Invalid group IRI ${forGroup.get}")
)
@@ -109,15 +113,15 @@ case class CreateDefaultObjectAccessPermissionAPIRequestADM(

forResourceClass match {
case Some(iri) =>
if (!stringFormatter.toSmartIri(iri).isKnoraEntityIri) {
if (!sf.toSmartIri(iri).isKnoraEntityIri) {
throw BadRequestException(s"Invalid resource class IRI: $iri")
}
case None => None
}

forProperty match {
case Some(iri) =>
if (!stringFormatter.toSmartIri(iri).isKnoraEntityIri) {
if (!sf.toSmartIri(iri).isKnoraEntityIri) {
throw BadRequestException(s"Invalid property IRI: $iri")
}
case None => None
@@ -7,6 +7,7 @@ package org.knora.webapi.messages.admin.responder.valueObjects

import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.groupsmessages.GroupsErrorMessagesADM._
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import zio.prelude.Validation
@@ -22,8 +23,12 @@ object GroupIRI { self =>
if (value.isEmpty) {
Validation.fail(BadRequestException(GROUP_IRI_MISSING_ERROR))
} else {
val isUUID: Boolean = sf.hasUUIDLength(value.split("/").last)

if (!sf.isKnoraGroupIriStr(value)) {
Validation.fail(BadRequestException(GROUP_IRI_INVALID_ERROR))
} else if (isUUID && !sf.isUUIDVersion4Or5(value)) {
Validation.fail(BadRequestException(UUID_INVALID_ERROR))
} else {
val validatedValue = Validation(
sf.validateAndEscapeIri(value, throw BadRequestException(GROUP_IRI_INVALID_ERROR))
@@ -7,7 +7,9 @@ package org.knora.webapi.messages.admin.responder.valueObjects

import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.listsmessages.ListsErrorMessagesADM._
import org.knora.webapi.messages.admin.responder.valueObjects.GroupIRI.sf
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import zio.prelude.Validation

@@ -22,8 +24,12 @@ object ListIRI { self =>
if (value.isEmpty) {
Validation.fail(BadRequestException(LIST_NODE_IRI_MISSING_ERROR))
} else {
val isUUID: Boolean = sf.hasUUIDLength(value.split("/").last)

if (!sf.isKnoraListIriStr(value)) {
Validation.fail(BadRequestException(LIST_NODE_IRI_INVALID_ERROR))
} else if (isUUID && !sf.isUUIDVersion4Or5(value)) {
Validation.fail(BadRequestException(UUID_INVALID_ERROR))
} else {
val validatedValue = Validation(
sf.validateAndEscapeIri(value, throw BadRequestException(LIST_NODE_IRI_INVALID_ERROR))
@@ -7,7 +7,9 @@ package org.knora.webapi.messages.admin.responder.valueObjects

import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsErrorMessagesADM._
import org.knora.webapi.messages.admin.responder.valueObjects.GroupIRI.sf
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import zio.prelude.Validation

@@ -16,14 +18,18 @@ import zio.prelude.Validation
*/
sealed abstract case class ProjectIRI private (value: String)
object ProjectIRI { self =>
val sf: StringFormatter = StringFormatter.getGeneralInstance
private val sf: StringFormatter = StringFormatter.getGeneralInstance

def make(value: String): Validation[Throwable, ProjectIRI] =
if (value.isEmpty) {
Validation.fail(BadRequestException(PROJECT_IRI_MISSING_ERROR))
} else {
val isUUID: Boolean = sf.hasUUIDLength(value.split("/").last)

if (!sf.isKnoraProjectIriStr(value)) {
Validation.fail(BadRequestException(PROJECT_IRI_INVALID_ERROR))
} else if (isUUID && !sf.isUUIDVersion4Or5(value)) {
Validation.fail(BadRequestException(UUID_INVALID_ERROR))
} else {
val validatedValue = Validation(
sf.validateAndEscapeProjectIri(value, throw BadRequestException(PROJECT_IRI_INVALID_ERROR))
@@ -8,7 +8,9 @@ package org.knora.webapi.messages.admin.responder.valueObjects
import org.knora.webapi.LanguageCodes
import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.usersmessages.UsersErrorMessagesADM._
import org.knora.webapi.messages.admin.responder.valueObjects.GroupIRI.sf
import zio.prelude.Validation

import scala.util.matching.Regex
@@ -24,8 +26,12 @@ object UserIRI { self =>
if (value.isEmpty) {
Validation.fail(BadRequestException(USER_IRI_MISSING_ERROR))
} else {
val isUUID: Boolean = sf.hasUUIDLength(value.split("/").last)

if (!sf.isKnoraUserIriStr(value)) {
Validation.fail(BadRequestException(USER_IRI_INVALID_ERROR))
} else if (isUUID && !sf.isUUIDVersion4Or5(value)) {
Validation.fail(BadRequestException(UUID_INVALID_ERROR))
} else {
val validatedValue = Validation(
sf.validateAndEscapeUserIri(value, throw BadRequestException(USER_IRI_INVALID_ERROR))
@@ -746,7 +746,7 @@ class XMLToStandoffUtil(
case Some(existingUuid) => existingUuid
case None =>
// Otherwise, try to parse the ID as a UUID.
if (stringFormatter.couldBeUuid(id)) {
if (stringFormatter.hasUUIDLength(id)) {
stringFormatter.decodeUuid(id)
} else {
// If the ID doesn't seem to be a UUID, replace it with a random UUID. TODO: this should throw an exception instead.
@@ -15,6 +15,7 @@ import org.knora.webapi._
import org.knora.webapi.exceptions._
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.projectsmessages.{
ProjectADM,
ProjectGetRequestADM,
@@ -724,12 +725,14 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource
requestingUser = requestingUser
)).mapTo[ProjectGetResponseADM]

_ = maybeCustomResourceIri.foreach { definedResourceIri =>
if (!definedResourceIri.isKnoraResourceIri) {
throw BadRequestException(s"<$definedResourceIri> is not a Knora resource IRI")
_ = maybeCustomResourceIri.foreach { iri =>
if (!iri.isKnoraResourceIri) {
throw BadRequestException(s"<$iri> is not a Knora resource IRI")
}

if (!definedResourceIri.getProjectCode.contains(projectInfoResponse.project.shortcode)) {
stringFormatter.validateUUIDOfResourceIRI(iri)

if (!iri.getProjectCode.contains(projectInfoResponse.project.shortcode)) {
throw BadRequestException(s"The provided resource IRI does not contain the correct project code")
}
}
@@ -930,6 +933,8 @@ object UpdateResourceMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Update
throw BadRequestException(s"Invalid resource IRI: <$resourceIri>")
}

stringFormatter.validateUUIDOfResourceIRI(resourceIri)

val resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri

val maybeLastModificationDate: Option[Instant] = jsonLDDocument.maybeDatatypeValueInObject(
@@ -7,7 +7,6 @@ package org.knora.webapi.messages.v2.responder.valuemessages

import java.time.Instant
import java.util.UUID

import akka.actor.ActorRef
import akka.event.LoggingAdapter
import akka.http.scaladsl.util.FastFuture
@@ -17,6 +16,7 @@ import org.knora.webapi._
import org.knora.webapi.exceptions.{AssertionException, BadRequestException, NotImplementedException, SipiException}
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.StringFormatter.UUID_INVALID_ERROR
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.store.sipimessages.{GetFileMetadataRequest, GetFileMetadataResponse}
@@ -500,6 +500,13 @@ object DeleteValueRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteValueReques
throw BadRequestException(s"Invalid value IRI: <$valueIri>")
}

if (
stringFormatter.hasUUIDLength(valueIri.toString.split("/").last)
&& !stringFormatter.isUUIDVersion4Or5(valueIri.toString)
) {
throw BadRequestException(UUID_INVALID_ERROR)
}

val valueTypeIri: SmartIri = jsonLDObject.requireTypeAsKnoraApiV2ComplexTypeIri

val deleteComment: Option[String] = jsonLDObject.maybeStringWithValidation(
@@ -88,7 +88,7 @@ class GroupsADME2ESpec extends E2ESpec(GroupsADME2ESpec.config) with GroupsADMJs
}

"given a custom Iri" should {
val customGroupIri = "http://rdfh.ch/groups/00FF/3eFYejZEduOCowwXQq5Iqg"
val customGroupIri = "http://rdfh.ch/groups/00FF/gNdJSNYrTDu2lGpPUs94nQ"
"create a group with the provided custom IRI " in {
val createGroupWithCustomIriRequest: String =
s"""{ "id": "$customGroupIri",
@@ -37,9 +37,8 @@ class PermissionsADME2ESpec extends E2ESpec(PermissionsADME2ESpec.config) with T

// Collects client test data
private val clientTestDataCollector = new ClientTestDataCollector(settings)
private val customDOAPIri = "http://rdfh.ch/permissions/00FF/eIAywlYBJA3a_5yI77UsMQ"
private val customDOAPIri = "http://rdfh.ch/permissions/00FF/zTOK3HlWTLGgTO8ZWVnotg"
"The Permissions Route ('admin/permissions')" when {

"getting permissions" should {
"return a group's administrative permission" in {

@@ -72,7 +71,6 @@ class PermissionsADME2ESpec extends E2ESpec(PermissionsADME2ESpec.config) with T
}

"return a project's administrative permissions" in {

val projectIri = java.net.URLEncoder.encode(SharedTestDataV1.imagesProjectInfo.id, "utf-8")

val request = Get(baseApiUrl + s"/admin/permissions/ap/$projectIri") ~> addCredentials(
@@ -98,7 +96,6 @@ class PermissionsADME2ESpec extends E2ESpec(PermissionsADME2ESpec.config) with T
}

"return a project's default object access permissions" in {

val projectIri = java.net.URLEncoder.encode(SharedTestDataV1.imagesProjectInfo.id, "utf-8")

val request = Get(baseApiUrl + s"/admin/permissions/doap/$projectIri") ~> addCredentials(
@@ -124,7 +121,6 @@ class PermissionsADME2ESpec extends E2ESpec(PermissionsADME2ESpec.config) with T
}

"return a project's all permissions" in {

val projectIri = java.net.URLEncoder.encode(SharedTestDataV1.imagesProjectInfo.id, "utf-8")

val request = Get(baseApiUrl + s"/admin/permissions/$projectIri") ~> addCredentials(

0 comments on commit 74d4344

Please sign in to comment.