Skip to content

Commit

Permalink
Add max JSON depth check to /keygen 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 c28f771 commit c5bbf0d
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ object Server {
val services: List[(String, RoutesConstructor)] = List(
"/api/meta" -> MetaService.asRoutes(debug, patchesAllowed, isHealthy),
"/api/schemas" -> SchemaService.asRoutes(patchesAllowed, webhook, maxJsonDepth),
"/api/auth" -> AuthService.asRoutes,
"/api/auth" -> AuthService.asRoutes(maxJsonDepth),
"/api/validation" -> ValidationService.asRoutes(maxJsonDepth),
"/api/drafts" -> DraftService.asRoutes(maxJsonDepth)
)
Expand Down
17 changes: 15 additions & 2 deletions src/main/scala/com/snowplowanalytics/iglu/server/Utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

package com.snowplowanalytics.iglu.server

import io.circe.{Encoder, Json}
import io.circe.{Decoder, Encoder, Json}
import io.circe.syntax._

import cats.implicits._
import cats.effect.Sync

import fs2.{Stream, text}

import org.http4s.{EntityDecoder, InvalidMessageBodyFailure}
import org.http4s.{DecodeResult, EntityDecoder, InvalidMessageBodyFailure}
import org.http4s.circe.jsonDecoder

import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaMap, SchemaVer}
Expand All @@ -31,6 +31,19 @@ object Utils {
def toBytes[F[_], A: Encoder](a: A): Stream[F, Byte] =
Stream.emit(a.asJson.noSpaces).through(text.utf8Encode)

def jsonOfWithDepthCheck[F[_]: Sync, A: Decoder](maxJsonDepth: Int): EntityDecoder[F, A] =
jsonDecoderWithDepthCheck(maxJsonDepth).flatMapR { json =>
json
.as[A]
.fold(
failure =>
DecodeResult.failureT[F, A](
InvalidMessageBodyFailure(s"Could not decode JSON body", Some(failure))
),
DecodeResult.successT[F, A](_)
)
}

def jsonDecoderWithDepthCheck[F[_]: Sync](maxJsonDepth: Int): EntityDecoder[F, Json] =
jsonDecoder[F].transform(
_.flatMap { json =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,24 @@ import com.snowplowanalytics.iglu.server.model.Permission
import com.snowplowanalytics.iglu.server.model.IgluResponse
import com.snowplowanalytics.iglu.server.storage.Storage

class AuthService[F[+_]: Sync](swagger: SwaggerSyntax[F], ctx: AuthedContext[F, Permission], db: Storage[F])
extends RhoRoutes[F] {
class AuthService[F[+_]: Sync](
swagger: SwaggerSyntax[F],
ctx: AuthedContext[F, Permission],
db: Storage[F],
maxJsonDepth: Int
) extends RhoRoutes[F] {
import swagger._
import AuthService._

val apikey = paramD[UUID]("key", "UUID apikey to delete")

val jsonGenerateKey = Utils.jsonOfWithDepthCheck[F, GenerateKey](maxJsonDepth)

"Route to delete api key" **
DELETE / "keygen" +? apikey >>> ctx.auth |>> deleteKey _

"Route to generate new keys" **
POST / "keygen" >>> ctx.auth ^ jsonOf[F, GenerateKey] |>> { (authInfo: Permission, gk: GenerateKey) =>
POST / "keygen" >>> ctx.auth ^ jsonGenerateKey |>> { (authInfo: Permission, gk: GenerateKey) =>
if (authInfo.key.contains(Permission.KeyAction.Create)) {
val vendorPrefix = Permission.Vendor.parse(gk.vendorPrefix)
if (authInfo.canCreatePermission(vendorPrefix.asString)) {
Expand All @@ -71,13 +77,13 @@ object AuthService {

implicit val schemaGenerateReq: Decoder[GenerateKey] = deriveDecoder[GenerateKey]

def asRoutes(
def asRoutes(maxJsonDepth: Int)(
db: Storage[IO],
superKey: Option[UUID],
ctx: AuthedContext[IO, Permission],
rhoMiddleware: RhoMiddleware[IO]
): HttpRoutes[IO] = {
val service = new AuthService(swaggerSyntax, ctx, db).toRoutes(rhoMiddleware)
val service = new AuthService(swaggerSyntax, ctx, db, maxJsonDepth).toRoutes(rhoMiddleware)
PermissionMiddleware.wrapService(db, superKey, ctx, service)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import cats.implicits._

import io.circe.Json

import org.http4s.{DecodeResult, HttpRoutes, InvalidMessageBodyFailure}
import org.http4s.HttpRoutes
import org.http4s.rho.{AuthedContext, RhoMiddleware, RhoRoutes}
import org.http4s.rho.swagger.SwaggerSyntax
import org.http4s.rho.swagger.syntax.{io => swaggerSyntax}
Expand Down Expand Up @@ -56,17 +56,7 @@ class SchemaService[F[+_]: Sync](
val version = pathVar[SchemaVer.Full]("version", "SchemaVer")
val isPublic = paramD[Boolean]("isPublic", false, "Should schema be created as public")

val schemaOrJson = Utils.jsonDecoderWithDepthCheck(maxJsonDepth).flatMapR { json =>
json
.as[SchemaBody]
.fold(
failure =>
DecodeResult.failureT[F, SchemaBody](
InvalidMessageBodyFailure(s"Could not decode JSON: ${json.noSpaces}", Some(failure))
),
DecodeResult.successT[F, SchemaBody](_)
)
}
val schemaOrJson = Utils.jsonOfWithDepthCheck[F, SchemaBody](maxJsonDepth)

val jsonBody = Utils.jsonDecoderWithDepthCheck(maxJsonDepth)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ import org.http4s.rho.swagger.syntax.io.createRhoMiddleware

import java.util.UUID
import com.snowplowanalytics.iglu.server.storage.InMemory
import com.snowplowanalytics.iglu.server.SpecHelpers._

class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec with InMemoryStorageSpec {
def getState(request: Request[IO], superApiKey: Option[UUID] = None): IO[(List[Response[IO]], InMemory.State)] =
def getState(
request: Request[IO],
superApiKey: Option[UUID] = None,
maxJsonDepth: Int = 20
): IO[(List[Response[IO]], InMemory.State)] =
sendRequestsGetState[InMemory.State](storage =>
AuthService.asRoutes(storage, superApiKey, SpecHelpers.ctx, createRhoMiddleware())
AuthService.asRoutes(maxJsonDepth)(storage, superApiKey, SpecHelpers.ctx, createRhoMiddleware())
)(storage => storage.asInstanceOf[InMemory[IO]].ref.get)(List(request))

def is = s2"""
Expand All @@ -32,6 +37,7 @@ class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec
/keygen doesn't authorize without apikey header $e4
/keygen doesn't authorize with unkown apikey in header $e5
/keygen deletes key $e6
/keygen rejects JSON body that exceeds maximum allowed JSON depth $e7
"""

def e1 = {
Expand Down Expand Up @@ -150,4 +156,31 @@ class AuthServiceSpec extends org.specs2.Specification with StorageAgnosticSpec

nokey.and(deletedResponse)
}

def e7 = {
val deepJsonSchema = createDeepJsonSchema(100000)
val deepJsonArray = createDeepJsonArray(1000000)
val wrongApikey = "c99ce0f9-cb5b-4b6f-88f3-2baed041be9b"

def executeTest(body: String, apikey: String) = {
val req = Request(
Method.POST,
Uri.uri("/keygen"),
headers = Headers.of(Header("apikey", apikey)),
body = toBytes(body)
)

val expected = List((422, "The request body was invalid."))

val (resp, _) = getState(req).unsafeRunSync()
val result = resp.map(r => (r.status.code, r.bodyText.compile.foldMonoid.unsafeRunSync()))

result must beEqualTo(expected)
}

executeTest(deepJsonSchema, SpecHelpers.superKey.toString)
executeTest(deepJsonArray, wrongApikey)
executeTest(deepJsonSchema, SpecHelpers.superKey.toString)
executeTest(deepJsonArray, wrongApikey)
}
}

0 comments on commit c5bbf0d

Please sign in to comment.