Skip to content

Prevent unauthorized access to the eval endpoint #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ lazy val evaluator = (project in file("."))
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"com.typesafe" % "config" % "1.3.0",
"com.pauldijou" %% "jwt-core" % "0.8.0",
"org.log4s" %% "log4s" % "1.3.0",
"org.slf4j" % "slf4j-simple" % "1.7.21",
"io.get-coursier" %% "coursier" % "1.0.0-M12",
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
eval.auth {
secretKey = "secretKey"
secretKey = ${?EVAL_SECRET_KEY}
}

57 changes: 57 additions & 0 deletions src/main/scala/auth.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.scalaexercises.evaluator

import org.http4s._, org.http4s.dsl._, org.http4s.server._
import com.typesafe.config._
import org.http4s.util._
import scala.util.{Try, Success, Failure}
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}

import scalaz.concurrent.Task

object auth {

val config = ConfigFactory.load()

val SecretKeyPath = "eval.auth.secretKey"

val secretKey = if (config.hasPath(SecretKeyPath)) {
config.getString(SecretKeyPath)
} else {
throw new IllegalStateException("Missing -Deval.auth.secretKey=[YOUR_KEY_HERE] or env var [EVAL_SECRET_KEY] ")
}

object `X-Scala-Eval-Api-Token` extends HeaderKey.Singleton {

type HeaderT = `X-Scala-Eval-Api-Token`

def name: CaseInsensitiveString = "x-scala-eval-api-token".ci

override def parse(s: String): ParseResult[`X-Scala-Eval-Api-Token`] =
ParseResult.success(`X-Scala-Eval-Api-Token`(s))

def matchHeader(header: Header): Option[HeaderT] = {
if (header.name == name) Some(`X-Scala-Eval-Api-Token`(header.value))
else None
}

}

final case class `X-Scala-Eval-Api-Token`(token: String) extends Header.Parsed {
override def key = `X-Scala-Eval-Api-Token`
override def renderValue(writer: Writer): writer.type =
writer.append(token)
}

def apply(service: HttpService): HttpService = Service.lift { req =>
req.headers.get(`X-Scala-Eval-Api-Token`) match {
case Some(header) =>
Jwt.decodeRaw(header.value, secretKey, Seq(JwtAlgorithm.HS256)) match {
case Success(_) => service(req)
case Failure(_) => Task.now(Response(Status.Unauthorized))
}
case None => Task.now(Response(Status.Unauthorized))
}

}

}
6 changes: 3 additions & 3 deletions src/main/scala/services.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object services {

val evaluator = new Evaluator(20 seconds)

def service = HttpService {
def evalService = auth(HttpService {
case req @ POST -> Root / "eval" =>
import io.circe.syntax._
req.decode[EvalRequest] { evalRequest =>
Expand All @@ -45,7 +45,7 @@ object services {
Ok(response.asJson)
}
}
}
})

}

Expand All @@ -66,7 +66,7 @@ object EvaluatorServer extends App {

BlazeBuilder
.bindHttp(port, ip)
.mountService(service)
.mountService(evalService)
.start
.run
.awaitShutdown()
Expand Down
45 changes: 35 additions & 10 deletions src/test/scala/EvalEndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,43 @@
package org.scalaexercises.evaluator

import org.scalatest._
import org.http4s._, org.http4s.dsl._, org.http4s.server._
import org.http4s._
import org.http4s.headers._
import org.http4s.dsl._
import org.http4s.server._

import io.circe.syntax._
import io.circe.generic.auto._
import scalaz.stream.Process.emit
import java.nio.charset.StandardCharsets
import scodec.bits.ByteVector
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}

import org.http4s.{Status => HttpStatus}

class EvalEndpointSpec extends FunSpec with Matchers {

import services._
import codecs._
import auth._
import EvalResponse.messages._

val sonatypeReleases = "https://oss.sonatype.org/content/repositories/releases/" :: Nil

def serve(evalRequest: EvalRequest) =
service.run(Request(
val validToken = Jwt.encode("""{"user": "scala-exercises"}""", auth.secretKey, JwtAlgorithm.HS256)

val invalidToken = java.util.UUID.randomUUID.toString

def serve(evalRequest: EvalRequest, authHeader : Header) =
evalService.run(Request(
POST,
Uri(path = "/eval"),
body = emit(
ByteVector.view(
evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8)
)
)
)).run
).putHeaders(authHeader)).run

def verifyEvalResponse(
response: Response,
Expand All @@ -50,7 +59,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
describe("evaluation") {
it("can evaluate simple expressions") {
verifyEvalResponse(
response = serve(EvalRequest(code = "{ 41 + 1 }")),
response = serve(EvalRequest(code = "{ 41 + 1 }"), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = Some("42"),
expectedMessage = `ok`
Expand All @@ -59,7 +68,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {

it("fails with a timeout when takes longer than the configured timeout") {
verifyEvalResponse(
response = serve(EvalRequest(code = "{ while(true) {}; 123 }")),
response = serve(EvalRequest(code = "{ while(true) {}; 123 }"), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = None,
expectedMessage = `Timeout Exceded`
Expand All @@ -72,7 +81,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
code = "{import cats._; Eval.now(42).value}",
resolvers = sonatypeReleases,
dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil
)),
), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = Some("42"),
expectedMessage = `ok`
Expand All @@ -89,7 +98,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
code = code,
resolvers = resolvers,
dependencies = Dependency("org.typelevel", "cats_2.11", version) :: Nil
)),
), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = Some("42"),
expectedMessage = `ok`
Expand All @@ -104,7 +113,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
code = "{import stdlib._; Asserts.scalaTestAsserts(true)}",
resolvers = sonatypeReleases,
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
)),
), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = Some("()"),
expectedMessage = `ok`
Expand All @@ -117,13 +126,29 @@ class EvalEndpointSpec extends FunSpec with Matchers {
code = "{import stdlib._; Asserts.scalaTestAsserts(false)}",
resolvers = sonatypeReleases,
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
)),
), `X-Scala-Eval-Api-Token`(validToken)),
expectedStatus = HttpStatus.Ok,
expectedValue = None,
expectedMessage = `Runtime Error`
)
}

it("rejects requests with invalid tokens") {
serve(EvalRequest(
code = "1",
resolvers = Nil,
dependencies = Nil
), `X-Scala-Eval-Api-Token`(invalidToken)).status should be (HttpStatus.Unauthorized)
}

it("rejects requests with missing tokens") {
serve(EvalRequest(
code = "1",
resolvers = Nil,
dependencies = Nil
), `Accept-Ranges`(Nil)).status should be (HttpStatus.Unauthorized)
}

}
}