Skip to content

Commit

Permalink
Add contextSecurityIn for http4s endpoints (#3939)
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurenceWarne authored Jul 19, 2024
1 parent b0f7640 commit 80f2061
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 32 deletions.
5 changes: 3 additions & 2 deletions doc/server/http4s.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ val routes = Http4sServerInterpreter[IO]().toRoutes(sseEndpoint.serverLogicSucce
## Accessing http4s context

If you'd like to access context provided by an http4s middleware, e.g. with authentication data, this can be done
with a dedicated context-extracting input, `.contextIn`. Endpoints using such input need then to be interpreted to
`org.http4s.ContextRoutes` (also known by its type alias `AuthedRoutes`) using the `.toContextRoutes` method.
with a dedicated context-extracting input, `.contextIn` (or the analogous `.contextSecurityIn` for security inputs).
Endpoints using such input need then to be interpreted to `org.http4s.ContextRoutes` (also known by its type alias
`AuthedRoutes`) using the `.toContextRoutes` method.

For example:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import scala.reflect.ClassTag
class Http4sInvalidWebSocketUse(val message: String) extends Exception

/** A capability that is used by endpoints, when they need to access the http4s-provided context. Such a requirement can be added using the
* [[RichHttp4sEndpoint.contextIn]] method.
* [[RichHttp4sEndpoint.contextIn]] or [[RichHttp4sEndpoint.contextSecurityIn]] methods.
*/
trait Context[T]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,34 @@ package object http4s {
*/
def contextIn[T]: AddContextInput[T] = new AddContextInput[T]

/** Access the context provided by an http4s middleware, such as authentication data.
*
* Interpreting endpoints which access the http4s context requires the usage of the [[Http4sServerInterpreter.toContextRoutes]] method.
* This then yields a [[org.http4s.ContextRoutes]] instance, which needs to be correctly mounted in the http4s router.
*
* Note that the correct syntax for adding the context input includes `()` after the method invocation, to properly infer types and
* capture implicit parameters, e.g. `myEndpoint.contextSecurityIn[Auth]()`.
*/
def contextSecurityIn[T]: AddContextSecurityInput[T] = new AddContextSecurityInput[T]

class AddContextInput[T] {
def apply[IT]()(implicit concat: ParamConcat.Aux[I, T, IT], ct: ClassTag[T]): Endpoint[A, IT, E, O, R with Context[T]] = {
val attribute = contextAttributeKey[T]
e.in(extractFromRequest[T] { (req: ServerRequest) =>
req
.attribute(attribute)
// should never happen since http4s had to build a ContextRequest with Ctx for ContextRoutes
.getOrElse(throw new RuntimeException(s"context ${attribute.typeName} not found in the request"))
})
e.in(extractFromRequest[T](extractContext[T](attribute)))
}
}

class AddContextSecurityInput[T] {
def apply[AT]()(implicit concat: ParamConcat.Aux[A, T, AT], ct: ClassTag[T]): Endpoint[AT, I, E, O, R with Context[T]] = {
val attribute = contextAttributeKey[T]
e.securityIn(extractFromRequest[T](extractContext[T](attribute)))
}
}

private def extractContext[T](attribute: AttributeKey[T]): ServerRequest => T = (req: ServerRequest) =>
req
.attribute(attribute)
// should never happen since http4s had to build a ContextRequest with Ctx for ContextRoutes
.getOrElse(throw new RuntimeException(s"context ${attribute.typeName} not found in the request"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.http4s.server.Router
import org.http4s.server.ContextMiddleware
import org.http4s.ContextRoutes
import org.http4s.HttpRoutes
import org.scalatest.OptionValues
import org.scalatest.{Assertion, OptionValues}
import org.scalatest.matchers.should.Matchers._
import sttp.capabilities.WebSockets
import sttp.capabilities.fs2.Fs2Streams
Expand Down Expand Up @@ -39,23 +39,25 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi
val sse1 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200)))
val sse2 = ServerSentEvent(randomUUID, randomUUID, randomUUID, Some(Random.nextInt(200)))

def blazeServerAssertions[T](routes: HttpRoutes[IO], expectedContext: T): IO[Assertion] = BlazeServerBuilder[IO]
.withExecutionContext(ExecutionContext.global)
.bindHttp(0, "localhost")
.withHttpApp(Router("/api" -> routes).orNotFound)
.resource
.use { server =>
val port = server.address.getPort
basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right(expectedContext))
}

def additionalTests(): List[Test] = List(
Test("should work with a router and routes in a context") {
val e = endpoint.get.in("test" / "router").out(stringBody).serverLogic(_ => IO.pure("ok".asRight[Unit]))
val expectedContent: String = "ok"
val e = endpoint.get.in("test" / "router").out(stringBody).serverLogic(_ => IO.pure(expectedContent.asRight[Unit]))
val routes = Http4sServerInterpreter[IO]().toRoutes(e)

BlazeServerBuilder[IO]
.withExecutionContext(ExecutionContext.global)
.bindHttp(0, "localhost")
.withHttpApp(Router("/api" -> routes).orNotFound)
.resource
.use { server =>
val port = server.address.getPort
basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right("ok"))
}
.unsafeRunSync()
blazeServerAssertions(routes, expectedContent).unsafeRunSync()
},
Test("should work with a router and context routes in a context") {
Test("should work with a router and context routes in a context") {
val expectedContext: String = "Hello World!" // the context we expect http4s to provide to the endpoint

val e: Endpoint[Unit, String, Unit, String, Context[String]] =
Expand All @@ -70,16 +72,21 @@ class Http4sServerTest[R >: Fs2Streams[IO] with WebSockets] extends TestSuite wi
val middleware: ContextMiddleware[IO, String] =
ContextMiddleware.const(expectedContext)

BlazeServerBuilder[IO]
.withExecutionContext(ExecutionContext.global)
.bindHttp(0, "localhost")
.withHttpApp(Router("/api" -> middleware(routesWithContext)).orNotFound)
.resource
.use { server =>
val port = server.address.getPort
basicRequest.get(uri"http://localhost:$port/api/test/router").send(backend).map(_.body shouldBe Right(expectedContext))
}
.unsafeRunSync()
blazeServerAssertions(middleware(routesWithContext), expectedContext).unsafeRunSync()
},
Test("should work with a router and context routes in a context using contextSecurityIn") {
val expectedContext: Int = 3

val e: Endpoint[Int, Unit, Unit, String, Context[Int]] =
endpoint.get.in("test" / "router").contextSecurityIn[Int]().out(stringBody)

val routesWithContext: ContextRoutes[Int, IO] =
Http4sServerInterpreter[IO]()
.toContextRoutes(e.serverSecurityLogicSuccess(IO.pure).serverLogicSuccess(x => _ => IO.pure(x.toString)))

val middleware: ContextMiddleware[IO, Int] = ContextMiddleware.const(expectedContext)

blazeServerAssertions(middleware(routesWithContext), expectedContext.toString).unsafeRunSync()
},
createServerTest.testServer(
endpoint.out(
Expand Down

0 comments on commit 80f2061

Please sign in to comment.