mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Authorize share access
This commit is contained in:
@ -10,7 +10,7 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.auth.{AuthToken, ShareToken}
|
||||
import docspell.common._
|
||||
import docspell.oidc.CodeFlowRoutes
|
||||
import docspell.restserver.auth.OpenId
|
||||
@ -44,9 +44,12 @@ object RestServer {
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
securedRoutes(cfg, restApp, token)
|
||||
},
|
||||
"/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
|
||||
"/api/v1/admin" -> AdminAuth(cfg.adminEndpoint) {
|
||||
adminRoutes(cfg, restApp)
|
||||
},
|
||||
"/api/v1/share" -> ShareAuth(restApp.backend.share, cfg.auth) { token =>
|
||||
shareRoutes(cfg, restApp, token)
|
||||
},
|
||||
"/api/doc" -> templates.doc,
|
||||
"/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F]),
|
||||
"/app" -> EnvMiddleware(templates.app),
|
||||
@ -120,7 +123,8 @@ object RestServer {
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||
"checkfile" -> CheckFileRoutes.open(restApp.backend),
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg),
|
||||
"share" -> ShareRoutes.verify(restApp.backend, cfg)
|
||||
)
|
||||
|
||||
def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
@ -132,6 +136,15 @@ object RestServer {
|
||||
"attachments" -> AttachmentRoutes.admin(restApp.backend)
|
||||
)
|
||||
|
||||
def shareRoutes[F[_]: Async](
|
||||
cfg: Config,
|
||||
restApp: RestApp[F],
|
||||
token: ShareToken
|
||||
): HttpRoutes[F] =
|
||||
Router(
|
||||
"search" -> ShareSearchRoutes(restApp.backend, cfg, token)
|
||||
)
|
||||
|
||||
def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.auth
|
||||
|
||||
import docspell.backend.auth.ShareToken
|
||||
import docspell.common._
|
||||
|
||||
import org.http4s._
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
final case class ShareCookieData(token: ShareToken) {
|
||||
def asString: String = token.asString
|
||||
|
||||
def asCookie(baseUrl: LenientUri): ResponseCookie = {
|
||||
val sec = baseUrl.scheme.exists(_.endsWith("s"))
|
||||
val path = baseUrl.path / "api" / "v1"
|
||||
ResponseCookie(
|
||||
name = ShareCookieData.cookieName,
|
||||
content = asString,
|
||||
domain = None,
|
||||
path = Some(path.asString),
|
||||
httpOnly = true,
|
||||
secure = sec,
|
||||
maxAge = None,
|
||||
expires = None
|
||||
)
|
||||
}
|
||||
|
||||
def addCookie[F[_]](baseUrl: LenientUri)(
|
||||
resp: Response[F]
|
||||
): Response[F] =
|
||||
resp.addCookie(asCookie(baseUrl))
|
||||
}
|
||||
|
||||
object ShareCookieData {
|
||||
val cookieName = "docspell_share"
|
||||
val headerName = "Docspell-Share-Auth"
|
||||
|
||||
def fromCookie[F[_]](req: Request[F]): Option[String] =
|
||||
for {
|
||||
header <- req.headers.get[headers.Cookie]
|
||||
cookie <- header.values.toList.find(_.name == cookieName)
|
||||
} yield cookie.content
|
||||
|
||||
def fromHeader[F[_]](req: Request[F]): Option[String] =
|
||||
req.headers
|
||||
.get(CIString(headerName))
|
||||
.map(_.head.value)
|
||||
|
||||
def fromRequest[F[_]](req: Request[F]): Option[String] =
|
||||
fromCookie(req).orElse(fromHeader(req))
|
||||
|
||||
def delete(baseUrl: LenientUri): ResponseCookie =
|
||||
ResponseCookie(
|
||||
name = cookieName,
|
||||
content = "",
|
||||
domain = None,
|
||||
path = Some(baseUrl.path / "api" / "v1").map(_.asString),
|
||||
httpOnly = true,
|
||||
secure = baseUrl.scheme.exists(_.endsWith("s")),
|
||||
maxAge = None,
|
||||
expires = None
|
||||
)
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.Password
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.http4s.Responses
|
||||
|
||||
@ -19,7 +20,7 @@ import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
object AdminRoutes {
|
||||
object AdminAuth {
|
||||
private val adminHeader = CIString("Docspell-Admin-Secret")
|
||||
|
||||
def apply[F[_]: Async](cfg: Config.AdminEndpoint)(
|
||||
@ -55,6 +56,5 @@ object AdminRoutes {
|
||||
req.headers.get(adminHeader).map(_.head.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 }
|
||||
Password(s1).compare(Password(s2))
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.auth.{Login, ShareToken}
|
||||
import docspell.backend.ops.OShare
|
||||
import docspell.backend.ops.OShare.VerifyResult
|
||||
import docspell.restserver.auth.ShareCookieData
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
|
||||
object ShareAuth {
|
||||
|
||||
def authenticateRequest[F[_]: Async](
|
||||
validate: String => F[VerifyResult]
|
||||
)(req: Request[F]): F[OShare.VerifyResult] =
|
||||
ShareCookieData.fromRequest(req) match {
|
||||
case Some(tokenStr) =>
|
||||
validate(tokenStr)
|
||||
case None =>
|
||||
VerifyResult.notFound.pure[F]
|
||||
}
|
||||
|
||||
private def getToken[F[_]: Async](
|
||||
auth: String => F[VerifyResult]
|
||||
): Kleisli[F, Request[F], Either[String, ShareToken]] =
|
||||
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
|
||||
|
||||
def of[F[_]: Async](S: OShare[F], cfg: Login.Config)(
|
||||
pf: PartialFunction[AuthedRequest[F, ShareToken], F[Response[F]]]
|
||||
): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getToken[F](S.verifyToken(cfg.serverSecret))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, ShareToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes.of(pf))
|
||||
}
|
||||
|
||||
def apply[F[_]: Async](S: OShare[F], cfg: Login.Config)(
|
||||
f: ShareToken => HttpRoutes[F]
|
||||
): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getToken[F](S.verifyToken(cfg.serverSecret))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, ShareToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req)))
|
||||
}
|
||||
}
|
@ -9,13 +9,18 @@ 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.backend.ops.OShare
|
||||
import docspell.backend.ops.OShare.VerifyResult
|
||||
import docspell.common.{Ident, Timestamp}
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.auth.ShareCookieData
|
||||
import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator}
|
||||
import docspell.store.records.RShare
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
@ -66,6 +71,31 @@ object ShareRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
def verify[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of { case req @ POST -> Root / "verify" =>
|
||||
for {
|
||||
secret <- req.as[ShareSecret]
|
||||
res <- backend.share
|
||||
.verify(cfg.auth.serverSecret)(secret.shareId, secret.password)
|
||||
resp <- res match {
|
||||
case VerifyResult.Success(token) =>
|
||||
val cd = ShareCookieData(token)
|
||||
Ok(ShareVerifyResult(true, token.asString, false, "Success"))
|
||||
.map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req)))
|
||||
case VerifyResult.PasswordMismatch =>
|
||||
Ok(ShareVerifyResult(false, "", true, "Failed"))
|
||||
case VerifyResult.NotFound =>
|
||||
Ok(ShareVerifyResult(false, "", false, "Failed"))
|
||||
case VerifyResult.InvalidToken =>
|
||||
Ok(ShareVerifyResult(false, "", false, "Failed"))
|
||||
}
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
|
||||
OShare.NewShare(
|
||||
user.account.collective,
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.ShareToken
|
||||
import docspell.common.Logger
|
||||
import docspell.restserver.Config
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
|
||||
object ShareSearchRoutes {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
backend: BackendApp[F],
|
||||
cfg: Config,
|
||||
token: ShareToken
|
||||
): HttpRoutes[F] = {
|
||||
val logger = Logger.log4s[F](org.log4s.getLogger)
|
||||
logger.trace(s"$backend $cfg $token")
|
||||
???
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user