Create a place for admin routes

And move re-creation of fulltext index in this place.
This commit is contained in:
Eike Kettner 2021-01-04 14:52:24 +01:00
parent f8735cbcdb
commit 306f064ad9
6 changed files with 89 additions and 12 deletions

View File

@ -111,6 +111,18 @@ docspell.server {
}
}
# This is a special endpoint that allows some basic administration.
#
# It is intended to be used by admins only, that is users who
# installed the app and have access to the system. Normal users
# should not have access and therefore a secret must be provided in
# order to access it.
admin-endpoint {
# The secret. If empty, the endpoint is disabled.
secret = ""
}
# Configuration of the full-text search engine.
full-text-search {
# The full-text search feature can be disabled. It requires an

View File

@ -18,13 +18,16 @@ case class Config(
integrationEndpoint: Config.IntegrationEndpoint,
maxItemPageSize: Int,
maxNoteLength: Int,
fullTextSearch: Config.FullTextSearch
fullTextSearch: Config.FullTextSearch,
adminEndpoint: Config.AdminEndpoint
)
object Config {
case class Bind(address: String, port: Int)
case class AdminEndpoint(secret: String)
case class IntegrationEndpoint(
enabled: Boolean,
priority: Priority,

View File

@ -35,6 +35,9 @@ object RestServer {
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
securedRoutes(cfg, pools, restApp, token)
},
"/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
adminRoutes(cfg, restApp)
},
"/api/doc" -> templates.doc,
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
"/app" -> templates.app,
@ -95,8 +98,12 @@ object RestServer {
"signup" -> RegisterRoutes(restApp.backend, cfg),
"upload" -> UploadRoutes.open(restApp.backend, cfg),
"checkfile" -> CheckFileRoutes.open(restApp.backend),
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg),
"fts" -> FullTextIndexRoutes.open(cfg, restApp.backend)
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
)
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
Router(
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend)
)
def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {

View File

@ -1,6 +1,8 @@
package docspell.restserver.http4s
import cats.data.NonEmptyList
import cats.data.OptionT
import cats.effect.Sync
import fs2.text.utf8Encode
import fs2.{Pure, Stream}
@ -36,4 +38,7 @@ object Responses {
)
)
def notFoundRoute[F[_]: Sync]: HttpRoutes[F] =
HttpRoutes(_ => OptionT.pure(Response.notFound[F]))
}

View File

@ -0,0 +1,54 @@
package docspell.restserver.routes
import cats.data.{Kleisli, OptionT}
import cats.effect._
import cats.implicits._
import docspell.restserver.Config
import docspell.restserver.http4s.Responses
import org.http4s._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
import org.http4s.server._
import org.http4s.util.CaseInsensitiveString
object AdminRoutes {
private val adminHeader = CaseInsensitiveString("Docspell-Admin-Secret")
def apply[F[_]: Effect](cfg: Config.AdminEndpoint)(
f: HttpRoutes[F]
): HttpRoutes[F] = {
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
import dsl._
val authUser = checkSecret[F](cfg)
val onFailure: AuthedRoutes[String, F] =
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
val middleware: AuthMiddleware[F, Unit] =
AuthMiddleware(authUser, onFailure)
if (cfg.secret.isEmpty) Responses.notFoundRoute[F]
else middleware(AuthedRoutes(authReq => f.run(authReq.req)))
}
private def checkSecret[F[_]: Effect](
cfg: Config.AdminEndpoint
): Kleisli[F, Request[F], Either[String, Unit]] =
Kleisli(req =>
extractSecret[F](req)
.filter(compareSecret(cfg.secret))
.toRight("Secret invalid")
.map(_ => ())
.pure[F]
)
private def extractSecret[F[_]](req: Request[F]): Option[String] =
req.headers.get(adminHeader).map(_.value)
private def compareSecret(s1: String)(s2: String): Boolean =
s1.length > 0 && s1.length == s2.length &&
s1.zip(s2).forall({ case (a, b) => a == b })
}

View File

@ -1,14 +1,13 @@
package docspell.restserver.routes
import cats.data.OptionT
import cats.effect._
import cats.implicits._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.common._
import docspell.restserver.Config
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s.Responses
import org.http4s._
import org.http4s.circe.CirceEntityEncoder._
@ -34,23 +33,20 @@ object FullTextIndexRoutes {
}
}
def open[F[_]: Effect](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
def admin[F[_]: Effect](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
if (!cfg.fullTextSearch.enabled) notFound[F]
else {
val dsl = Http4sDsl[F]
import dsl._
HttpRoutes.of { case POST -> Root / "reIndexAll" / Ident(id) =>
HttpRoutes.of { case POST -> Root / "reIndexAll" =>
for {
res <-
if (id.nonEmpty && id == cfg.fullTextSearch.recreateKey)
backend.fulltext.reindexAll.attempt
else Left(new Exception("The provided key is invalid.")).pure[F]
res <- backend.fulltext.reindexAll.attempt
resp <- Ok(Conversions.basicResult(res, "Full-text index will be re-created."))
} yield resp
}
}
private def notFound[F[_]: Effect]: HttpRoutes[F] =
HttpRoutes(_ => OptionT.pure(Response.notFound[F]))
Responses.notFoundRoute[F]
}