Skip to content

Commit

Permalink
Add auth to validation endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
spenes authored and AlexBenny committed Jan 23, 2025
1 parent 7ba76fe commit b941a0f
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ case class Permission(
/** Check if user has enough rights to create particular schema */
def canCreatePermission(requestedVendor: String): Boolean =
key.contains(Permission.KeyAction.Create) && vendor.check(requestedVendor)

/** It is enough to have any valid apikey to perform validation */
def canValidate: Boolean =
schema.nonEmpty || key.nonEmpty
}

object Permission {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ class SchemaService[F[+_]: Sync](
POST +? isPublic >>> ctx.auth ^ schemaOrJson |>> postSchema _

"Schema validation endpoint (deprecated)" **
POST / "validate" / 'vendor / 'name / "jsonschema" / 'version ^ jsonDecoder[F] |>> {
(_: String, _: String, _: String, json: Json) => validationService.validateSchema(Schema.Format.Jsonschema, json)
POST / "validate" / 'vendor / 'name / "jsonschema" / 'version >>> ctx.auth ^ jsonDecoder[F] |>> {
(_: String, _: String, _: String, authInfo: Permission, json: Json) =>
validationService.validateSchema(Schema.Format.Jsonschema, authInfo, json)
}

def getSchema(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,45 +53,53 @@ class ValidationService[F[+_]: Sync](
val schemaFormat = pathVar[Schema.Format]("format", "Schema format, e.g. jsonschema")

"This route allows you to validate schemas" **
POST / "validate" / "schema" / schemaFormat ^ jsonDecoder[F] |>> validateSchema _
POST / "validate" / "schema" / schemaFormat >>> ctx.auth ^ jsonDecoder[F] |>> validateSchema _

"This route allows you to validate self-describing instances" **
POST / "validate" / "instance" >>> ctx.auth ^ jsonDecoder[F] |>> validateData _

def validateSchema(format: Schema.Format, schema: Json) =
format match {
case Schema.Format.Jsonschema =>
validateJsonSchema(schema, maxJsonDepth) match {
case Validated.Valid(sd) =>
val message = s"The schema provided is a valid self-describing ${sd.self.schemaKey.toSchemaUri} schema"
Ok(IgluResponse.Message(message): IgluResponse)
case Validated.Invalid(report) =>
Ok(IgluResponse.SchemaValidationReport(report): IgluResponse)
}
def validateSchema(format: Schema.Format, authInfo: Permission, schema: Json) =
if (!authInfo.canValidate) {
Forbidden("")
} else {
format match {
case Schema.Format.Jsonschema =>
validateJsonSchema(schema, maxJsonDepth) match {
case Validated.Valid(sd) =>
val message = s"The schema provided is a valid self-describing ${sd.self.schemaKey.toSchemaUri} schema"
Ok(IgluResponse.Message(message): IgluResponse)
case Validated.Invalid(report) =>
Ok(IgluResponse.SchemaValidationReport(report): IgluResponse)
}
}
}

def validateData(authInfo: Permission, instance: Json) =
SelfDescribingData.parse(instance) match {
case Right(SelfDescribingData(key, data)) =>
for {
schema <- db.getSchema(SchemaMap(key))
response <- schema match {
case Some(Schema(_, meta, schemaBody, _)) if meta.isPublic || authInfo.canRead(key.vendor) =>
CirceValidator.validate(data, schemaBody) match {
case Left(ValidatorError.InvalidData(report)) =>
Ok(IgluResponse.InstanceValidationReport(report): IgluResponse)
case Left(ValidatorError.InvalidSchema(_)) =>
val message = s"Schema ${key.toSchemaUri} fetched from DB is invalid"
InternalServerError(IgluResponse.Message(message): IgluResponse)
case Right(_) =>
Ok(IgluResponse.Message(s"Instance is valid ${key.toSchemaUri}"): IgluResponse)
}
case _ =>
NotFound(IgluResponse.SchemaNotFound: IgluResponse)
}
} yield response
case Left(error) =>
BadRequest(IgluResponse.Message(s"JSON payload is not self-describing, ${error.code}"): IgluResponse)
if (!authInfo.canValidate) {
NotFound(IgluResponse.SchemaNotFound: IgluResponse)
} else {
SelfDescribingData.parse(instance) match {
case Right(SelfDescribingData(key, data)) =>
for {
schema <- db.getSchema(SchemaMap(key))
response <- schema match {
case Some(Schema(_, meta, schemaBody, _)) if meta.isPublic || authInfo.canRead(key.vendor) =>
CirceValidator.validate(data, schemaBody) match {
case Left(ValidatorError.InvalidData(report)) =>
Ok(IgluResponse.InstanceValidationReport(report): IgluResponse)
case Left(ValidatorError.InvalidSchema(_)) =>
val message = s"Schema ${key.toSchemaUri} fetched from DB is invalid"
InternalServerError(IgluResponse.Message(message): IgluResponse)
case Right(_) =>
Ok(IgluResponse.Message(s"Instance is valid ${key.toSchemaUri}"): IgluResponse)
}
case _ =>
NotFound(IgluResponse.SchemaNotFound: IgluResponse)
}
} yield response
case Left(error) =>
BadRequest(IgluResponse.Message(s"JSON payload is not self-describing, ${error.code}"): IgluResponse)
}
}

}
Expand Down
17 changes: 10 additions & 7 deletions src/test/scala/com/snowplowanalytics/iglu/server/SpecHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ object SpecHelpers {

val ctx = new AuthedContext[IO, Permission]

val now = Instant.ofEpochMilli(1537621061000L)
val superKey = UUID.fromString("4ed2d87a-6da5-48e8-a23b-36a26e61f974")
val readKey = UUID.fromString("1eaad173-1da5-eef8-a2cb-3fa26e61f975")
val readKeyAcme = UUID.fromString("2abad125-0ba1-faf2-b2cc-4fa26e61f971")
val now = Instant.ofEpochMilli(1537621061000L)
val superKey = UUID.fromString("4ed2d87a-6da5-48e8-a23b-36a26e61f974")
val readKey = UUID.fromString("1eaad173-1da5-eef8-a2cb-3fa26e61f975")
val readKeyAcme = UUID.fromString("2abad125-0ba1-faf2-b2cc-4fa26e61f971")
val readKeyAcme2 = UUID.fromString("930fecd3-dc01-4e85-a718-7bf53ba8e4cd")
val nonExistentKey = UUID.fromString("e8f1161f-5fbc-46df-b0ea-063f025e20c9")

val schemaZero = json"""{"type": "object", "properties": {"one": {}}}"""
val selfSchemaZero =
Expand Down Expand Up @@ -73,9 +75,10 @@ object SpecHelpers {
schemas,
Map.empty,
Map(
superKey -> Permission.Super,
readKey -> Permission.ReadOnlyAny,
readKeyAcme -> Permission(Vendor(List("com", "acme"), false), Some(SchemaAction.Read), Set.empty)
superKey -> Permission.Super,
readKey -> Permission.ReadOnlyAny,
readKeyAcme -> Permission(Vendor(List("com", "acme"), false), Some(SchemaAction.Read), Set.empty),
readKeyAcme2 -> Permission(Vendor(List("com", "acme2"), false), Some(SchemaAction.Read), Set.empty)
),
drafts
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
POST /validate/schema/jsonschema reports malformed JSON Schema on unknown properties $e11
POST /validate/schema/jsonschema reports about invalid schema name $e12
POST /validate/schema/jsonschema reports about schema that exceeds maximum allowed JSON depth $e13
POST /validate/schema/jsonschema returns error when given apikey doesn't exist $e14
POST /validate/schema/jsonschema performs as expected with any existing key $e15

POST /validate/instance reports invalid instance for the root of an instance $e6
POST /validate/instance reports valid instance $e7
POST /validate/instance returns 404 Schema not found if schema does not exist $e8
POST /validate/instance validates an instance with private schema if apikey is appropriate $e9
POST /validate/instance pretends a private schema does not exist if apikey is inappropriate $e10
POST /validate/instance pretends a private schema does not exist if apikey doesn't exist $e16
"""

def e1 = {
Expand All @@ -77,6 +80,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand All @@ -103,6 +107,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
json"""{"message" : "The schema provided is a valid self-describing iglu:com.acme/nonexistent/jsonschema/1-0-0 schema"}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand Down Expand Up @@ -133,6 +138,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand All @@ -153,6 +159,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand All @@ -164,7 +171,9 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
def e5 = {
val expected = "The request body was malformed."
val request =
Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema")).withBodyStream(Stream.emits("non-json".getBytes))
Request[IO](Method.POST, Uri.uri("/validate/schema/jsonschema"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withBodyStream(Stream.emits("non-json".getBytes))

val response = sendRequestGetText(request)

Expand All @@ -186,7 +195,9 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
]
}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance))
val request = Request[IO](Method.POST, Uri.uri("/validate/instance"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withBodyStream(toBytes(instance))
val response = sendRequest(request)

response must beEqualTo(expected)
Expand All @@ -197,7 +208,9 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
json"""{"schema" : "iglu:com.acme/event/jsonschema/1-0-0", "data" : {"one": null} } """
val expected = json"""{"message" : "Instance is valid iglu:com.acme/event/jsonschema/1-0-0"}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance))
val request = Request[IO](Method.POST, Uri.uri("/validate/instance"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withBodyStream(toBytes(instance))
val response = sendRequest(request)

response must beEqualTo(expected)
Expand All @@ -209,7 +222,9 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
val expected =
json"""{"message" : "The schema is not found"}"""

val request = Request[IO](Method.POST, Uri.uri("/validate/instance")).withBodyStream(toBytes(instance))
val request = Request[IO](Method.POST, Uri.uri("/validate/instance"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withBodyStream(toBytes(instance))

val (responses, _) = sendRequests(List(request)).unsafeRunSync()
val response = responses.last
Expand All @@ -236,7 +251,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
val expected =
json"""{"message" : "The schema is not found"}"""
val request = Request[IO](Method.POST, Uri.uri("/validate/instance"))
.withHeaders(Headers.of(Header("apikey", "00000000-1111-eeee-0000-eeeeeeeeffff")))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKeyAcme2.toString)))
.withBodyStream(toBytes(instance))

val (responses, _) = sendRequests(List(request)).unsafeRunSync()
Expand Down Expand Up @@ -279,6 +294,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand Down Expand Up @@ -314,6 +330,7 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

Expand Down Expand Up @@ -368,11 +385,90 @@ class ValidationServiceSpec extends org.specs2.Specification with StorageAgnosti
}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

val response = sendRequest(request, 5)

response must beEqualTo(expected)
}

def e14 = {
val selfDescribingSchema = json"""
{
"$$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"self": {
"vendor": "com.acme",
"name": "nonexistent",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"description": "schema with no issues",
"properties": { }
}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

val requests = List(
request,
request.withHeaders(Headers.of(Header("apikey", SpecHelpers.nonExistentKey.toString)))
)

val responses = sendRequests(requests).map { case (responses, _) => responses.map(_.status.code) }.unsafeRunSync()

responses must beEqualTo(List(403, 403))
}

def e15 = {
val selfDescribingSchema = json"""
{
"$$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"self": {
"vendor": "com.acme",
"name": "nonexistent",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"description": "schema with no issues",
"properties": { }
}"""

val request = Request[IO](Method.POST, uri"/validate/schema/jsonschema")
.withHeaders(Headers.of(Header("apikey", SpecHelpers.superKey.toString)))
.withContentType(headers.`Content-Type`(MediaType.application.json))
.withBodyStream(toBytes(selfDescribingSchema))

val requests = List(
request.withHeaders(Headers.of(Header("apikey", SpecHelpers.superKey.toString))),
request.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKey.toString))),
request.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKeyAcme.toString))),
request.withHeaders(Headers.of(Header("apikey", SpecHelpers.readKeyAcme2.toString)))
)

val responses = sendRequests(requests).map { case (responses, _) => responses.map(_.status.code) }.unsafeRunSync()

responses must beEqualTo(List(200, 200, 200, 200))
}

def e16 = {
val instance =
json"""{"schema" : "iglu:com.acme/secret/jsonschema/1-0-0", "data" : {} } """
val expected =
json"""{"message" : "The schema is not found"}"""
val request = Request[IO](Method.POST, Uri.uri("/validate/instance"))
.withHeaders(Headers.of(Header("apikey", SpecHelpers.nonExistentKey.toString)))
.withBodyStream(toBytes(instance))

val (responses, _) = sendRequests(List(request)).unsafeRunSync()
val response = responses.last

val bodyExpectation = response.as[Json].unsafeRunSync() must beEqualTo(expected)
val statusExpectation = response.status.code must beEqualTo(404)
bodyExpectation.and(statusExpectation)
}
}

0 comments on commit b941a0f

Please sign in to comment.