mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Make internal endpoints available to nodes only
This commit is contained in:
@ -14,6 +14,7 @@ import docspell.oidc.ProviderConfig
|
||||
import docspell.pubsub.naive.PubSubConfig
|
||||
import docspell.restserver.Config.OpenIdConfig
|
||||
import docspell.restserver.auth.OpenId
|
||||
import docspell.restserver.http4s.InternalHeader
|
||||
|
||||
import com.comcast.ip4s.IpAddress
|
||||
|
||||
@ -35,8 +36,13 @@ case class Config(
|
||||
def openIdEnabled: Boolean =
|
||||
openid.exists(_.enabled)
|
||||
|
||||
def pubSubConfig: PubSubConfig =
|
||||
PubSubConfig(appId, baseUrl / "internal" / "pubsub", 100)
|
||||
def pubSubConfig(headerValue: Ident): PubSubConfig =
|
||||
PubSubConfig(
|
||||
appId,
|
||||
baseUrl / "internal" / "pubsub",
|
||||
100,
|
||||
InternalHeader.header(headerValue.id)
|
||||
)
|
||||
}
|
||||
|
||||
object Config {
|
||||
|
@ -19,12 +19,13 @@ import docspell.common._
|
||||
import docspell.oidc.CodeFlowRoutes
|
||||
import docspell.pubsub.naive.NaivePubSub
|
||||
import docspell.restserver.auth.OpenId
|
||||
import docspell.restserver.http4s.EnvMiddleware
|
||||
import docspell.restserver.http4s.{EnvMiddleware, InternalHeader}
|
||||
import docspell.restserver.routes._
|
||||
import docspell.restserver.webapp._
|
||||
import docspell.restserver.ws.OutputEvent.KeepAlive
|
||||
import docspell.restserver.ws.{OutputEvent, WebSocketRoutes}
|
||||
import docspell.store.Store
|
||||
import docspell.store.records.RInternalSetting
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.blaze.client.BlazeClientBuilder
|
||||
@ -50,14 +51,14 @@ object RestServer {
|
||||
server =
|
||||
Stream
|
||||
.resource(createApp(cfg, pools))
|
||||
.flatMap { case (restApp, pubSub, httpClient) =>
|
||||
.flatMap { case (restApp, pubSub, httpClient, setting) =>
|
||||
Stream(
|
||||
Subscriptions(wsTopic, restApp.backend.pubSub),
|
||||
BlazeServerBuilder[F]
|
||||
.bindHttp(cfg.bind.port, cfg.bind.address)
|
||||
.withoutBanner
|
||||
.withHttpWebSocketApp(
|
||||
createHttpApp(cfg, httpClient, pubSub, restApp, wsTopic)
|
||||
createHttpApp(cfg, setting, httpClient, pubSub, restApp, wsTopic)
|
||||
)
|
||||
.serve
|
||||
.drain
|
||||
@ -71,7 +72,7 @@ object RestServer {
|
||||
def createApp[F[_]: Async](
|
||||
cfg: Config,
|
||||
pools: Pools
|
||||
): Resource[F, (RestApp[F], NaivePubSub[F], Client[F])] =
|
||||
): Resource[F, (RestApp[F], NaivePubSub[F], Client[F], RInternalSetting)] =
|
||||
for {
|
||||
httpClient <- BlazeClientBuilder[F].resource
|
||||
store <- Store.create[F](
|
||||
@ -79,12 +80,18 @@ object RestServer {
|
||||
cfg.backend.files.chunkSize,
|
||||
pools.connectEC
|
||||
)
|
||||
pubSub <- NaivePubSub(cfg.pubSubConfig, store, httpClient)(Topics.all.map(_.topic))
|
||||
setting <- Resource.eval(store.transact(RInternalSetting.create))
|
||||
pubSub <- NaivePubSub(
|
||||
cfg.pubSubConfig(setting.internalRouteKey),
|
||||
store,
|
||||
httpClient
|
||||
)(Topics.all.map(_.topic))
|
||||
restApp <- RestAppImpl.create[F](cfg, store, httpClient, pubSub)
|
||||
} yield (restApp, pubSub, httpClient)
|
||||
} yield (restApp, pubSub, httpClient, setting)
|
||||
|
||||
def createHttpApp[F[_]: Async](
|
||||
cfg: Config,
|
||||
internSettings: RInternalSetting,
|
||||
httpClient: Client[F],
|
||||
pubSub: NaivePubSub[F],
|
||||
restApp: RestApp[F],
|
||||
@ -94,7 +101,9 @@ object RestServer {
|
||||
) = {
|
||||
val templates = TemplateRoutes[F](cfg)
|
||||
val httpApp = Router(
|
||||
"/internal/pubsub" -> pubSub.receiveRoute,
|
||||
"/internal" -> InternalHeader(internSettings.internalRouteKey) {
|
||||
internalRoutes(pubSub)
|
||||
},
|
||||
"/api/info" -> routes.InfoRoutes(),
|
||||
"/api/v1/open/" -> openRoutes(cfg, httpClient, restApp),
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
@ -116,6 +125,11 @@ object RestServer {
|
||||
Logger.httpApp(logHeaders = false, logBody = false)(httpApp)
|
||||
}
|
||||
|
||||
def internalRoutes[F[_]: Async](pubSub: NaivePubSub[F]): HttpRoutes[F] =
|
||||
Router(
|
||||
"pubsub" -> pubSub.receiveRoute
|
||||
)
|
||||
|
||||
def securedRoutes[F[_]: Async](
|
||||
cfg: Config,
|
||||
restApp: RestApp[F],
|
||||
|
@ -8,6 +8,7 @@ package docspell.restserver
|
||||
|
||||
import fs2.Stream
|
||||
import fs2.concurrent.Topic
|
||||
|
||||
import docspell.backend.msg.JobDone
|
||||
import docspell.common.ProcessItemArgs
|
||||
import docspell.pubsub.api.PubSubT
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect.kernel.Async
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.Ident
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server.AuthMiddleware
|
||||
import org.typelevel.ci._
|
||||
|
||||
// duplicated in joex project
|
||||
object InternalHeader {
|
||||
|
||||
private val headerName = ci"Docspell-Internal-Api"
|
||||
|
||||
def header(value: String): Header.Raw =
|
||||
Header.Raw(headerName, value)
|
||||
|
||||
def apply[F[_]: Async](key: Ident)(routes: HttpRoutes[F]): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = checkSecret[F](key)
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(NotFound(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, Unit] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes(authReq => routes.run(authReq.req)))
|
||||
}
|
||||
|
||||
private def checkSecret[F[_]: Async](
|
||||
key: Ident
|
||||
): Kleisli[F, Request[F], Either[String, Unit]] =
|
||||
Kleisli(req =>
|
||||
extractSecret[F](req)
|
||||
.filter(compareSecret(key.id))
|
||||
.toRight("Secret invalid")
|
||||
.map(_ => ())
|
||||
.pure[F]
|
||||
)
|
||||
|
||||
private def extractSecret[F[_]](req: Request[F]): Option[String] =
|
||||
req.headers.get(headerName).map(_.head.value)
|
||||
|
||||
private def compareSecret(s1: String)(s2: String): Boolean =
|
||||
s1 == s2
|
||||
}
|
Reference in New Issue
Block a user