Skip to content

Commit

Permalink
Disallow non-sequential version (close #135)
Browse files Browse the repository at this point in the history
  • Loading branch information
oguzhanunlu authored and spenes committed Oct 17, 2023
1 parent f1204d1 commit e8208ec
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ trait IgluResponse extends Product with Serializable {

object IgluResponse {

val NotFoundSchema = "The schema is not found"
val NotAuthorized = "Authentication error: not authorized"
val Mismatch = "Mismatch: the schema metadata does not match the payload and URI"
val DecodeError = "Cannot decode JSON schema"
val SchemaInvalidationMessage = "The schema does not conform to a JSON Schema v4 specification"
val DataInvalidationMessage = "The data for a field instance is invalid against its schema"
val NotFoundEndpoint = "The endpoint does not exist"
val NotFoundSchema = "The schema is not found"
val NotAuthorized = "Authentication error: not authorized"
val Mismatch = "Mismatch: the schema metadata does not match the payload and URI"
val DecodeError = "Cannot decode JSON schema"
val SchemaInvalidationMessage = "The schema does not conform to a JSON Schema v4 specification"
val DataInvalidationMessage = "The data for a field instance is invalid against its schema"
val NotFoundEndpoint = "The endpoint does not exist"
val NonSequentialSchemaVersion = "The schema version is not sequential"

case object SchemaNotFound extends IgluResponse
case object SchemaNonSequential extends IgluResponse
case object EndpointNotFound extends IgluResponse
case object InvalidSchema extends IgluResponse
case class SchemaMismatch(uriSchemaKey: SchemaKey, payloadSchemaKey: SchemaKey) extends IgluResponse
Expand Down Expand Up @@ -96,6 +98,8 @@ object IgluResponse {
)
case InvalidSchema =>
Json.fromFields(List("message" -> Json.fromString(DecodeError)))
case SchemaNonSequential =>
Json.fromFields(List("message" -> Json.fromString(NonSequentialSchemaVersion)))
case SchemaValidationReport(report) =>
Json.fromFields(
List(
Expand Down Expand Up @@ -129,8 +133,9 @@ object IgluResponse {

implicit val responsesDecoder: Decoder[IgluResponse] = Decoder.instance { cur =>
cur.downField("message").as[String] match {
case Right(NotFoundSchema) => SchemaNotFound.asRight
case Right(NotAuthorized) => Forbidden.asRight
case Right(NotFoundSchema) => SchemaNotFound.asRight
case Right(NonSequentialSchemaVersion) => SchemaNonSequential.asRight
case Right(NotAuthorized) => Forbidden.asRight
case Right(Mismatch) =>
for {
uriSchemaKey <- cur.downField("uriSchemaKey").as[SchemaKey]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ object VersionCursor {
case object PreviousMissing extends Inconsistency
case object SupersededMissing extends Inconsistency
case object SupersededInvalid extends Inconsistency
case object NextRevisionExists extends Inconsistency
case object AlreadyExists extends Inconsistency
case class Availability(isPublic: Boolean, previousPublic: Boolean) extends Inconsistency

Expand All @@ -51,6 +52,8 @@ object VersionCursor {
s"Superseded schema version(s) do not exist"
case SupersededInvalid =>
s"Superseded schema version(s) must be below the superseding version"
case NextRevisionExists =>
"Next revision in the group exists, check that schemas are published sequentially"
case AlreadyExists =>
"Schema already exists"
case Availability(isPublic, previousPublic) =>
Expand All @@ -65,7 +68,7 @@ object VersionCursor {
supersedes: List[SchemaVer.Full]
): Either[Inconsistency, Unit] =
if (existing.contains(version) && !patchesAllowed) Inconsistency.AlreadyExists.asLeft
else if (!previousExists(existing, get(version))) Inconsistency.PreviousMissing.asLeft
else if (isVersionAllowed(existing, get(version)).isLeft) isVersionAllowed(existing, get(version))
else if (supersedes.diff(existing).nonEmpty) Inconsistency.SupersededMissing.asLeft
else if (supersedes.exists(Ordering[SchemaVer.Full].gt(_, version))) Inconsistency.SupersededInvalid.asLeft
else ().asRight
Expand All @@ -81,18 +84,25 @@ object VersionCursor {
* Check if existing state allows new schema
* It makes an assumption that `existing` is entirely consistent list without `current` schema
*/
private[model] def previousExists(existing: List[SchemaVer.Full], current: VersionCursor) =
private[model] def isVersionAllowed(
existing: List[SchemaVer.Full],
current: VersionCursor
): Either[Inconsistency, Unit] =
current match {
case Initial => // We can always create a new schema group (vendor/name/1-0-0)
true
().asRight
case StartModel(m) =>
existing.map(_.model).contains(m - 1)
Either.cond(existing.map(_.model).contains(m - 1), (), Inconsistency.PreviousMissing)
case StartRevision(m, r) =>
val thisModel = existing.filter(_.model == m)
thisModel.map(_.revision).contains(r - 1)
Either.cond(thisModel.map(_.revision).contains(r - 1), (), Inconsistency.PreviousMissing)
case NonInitial(version) =>
val thisModel = existing.filter(_.model == version.model)
val thisRevision = thisModel.filter(_.revision == version.revision)
thisRevision.map(_.addition).contains(version.addition - 1)
val thisModel = existing.filter(_.model == version.model)
val thisRevision = thisModel.filter(_.revision == version.revision)
val sequentialAddition = thisRevision.map(_.addition).contains(version.addition - 1)
val nextRevision = thisModel.map(_.revision).contains(version.revision + 1)
if (nextRevision) Inconsistency.NextRevisionExists.asLeft
else if (!sequentialAddition) Inconsistency.PreviousMissing.asLeft
else ().asRight
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ class SchemaService[F[+_]: Sync](
_ <- webhooks.schemaPublished(schema.self.schemaKey, existing)
response <- if (existing) Ok(payload) else Created(payload)
} yield response
case Left(Inconsistency.NextRevisionExists) =>
Conflict(IgluResponse.SchemaNonSequential: IgluResponse)
case Left(error) =>
Conflict(IgluResponse.Message(error.show): IgluResponse)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,50 @@
package com.snowplowanalytics.iglu.server.model

import com.snowplowanalytics.iglu.core.SchemaVer
import com.snowplowanalytics.iglu.server.model.VersionCursor.Inconsistency

class VersionCursorSpec extends org.specs2.Specification {
def is = s2"""
previousExists validates new revision $e1
previousExists validates new model if no schemas were created for this model yet $e2
previousExists rejects new model if previous model does not exist yet $e3
previousExists validates new addition $e4
previousExists rejects new addition if previous does not exist $e5
isVersionAllowed validates new revision $e1
isVersionAllowed validates new model if no schemas were created for this model yet $e2
isVersionAllowed rejects new model if previous model does not exist yet $e3
isVersionAllowed validates new addition $e4
isVersionAllowed rejects new addition if previous does not exist $e5
isAllowed allows overriding schema if patchesAllowed set to true $e6
isAllowed forbids overriding schema if patchesAllowed set to false $e7
isAllowed forbids superseding nonexistent schema versions $e8
isAllowed forbids superseding superior schema versions $e9
isVersionAllowed rejects new addition if next revision exists $e10
"""

def e1 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 0, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 0))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e2 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 0, 1))
val current = VersionCursor.get(SchemaVer.Full(2, 0, 0))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e3 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0))
val current = VersionCursor.get(SchemaVer.Full(3, 0, 0))
VersionCursor.previousExists(existing, current) must beFalse
VersionCursor.isVersionAllowed(existing, current) must beLeft
}

def e4 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 2))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e5 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 3))
VersionCursor.previousExists(existing, current) must beFalse
VersionCursor.isVersionAllowed(existing, current) must beLeft
}

def e6 =
Expand Down Expand Up @@ -86,4 +88,10 @@ class VersionCursorSpec extends org.specs2.Specification {
) must beLeft(
VersionCursor.Inconsistency.SupersededInvalid
)

def e10 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0))
val current = VersionCursor.get(SchemaVer.Full(1, 0, 1))
VersionCursor.isVersionAllowed(existing, current) must beLeft(Inconsistency.NextRevisionExists)
}
}

0 comments on commit e8208ec

Please sign in to comment.