Authorize share access

This commit is contained in:
eikek
2021-10-03 23:56:59 +02:00
parent 97922340d9
commit f4596db63d
13 changed files with 457 additions and 11 deletions

View File

@ -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._

View File

@ -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
)
}

View File

@ -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))
}

View File

@ -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)))
}
}

View File

@ -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,

View File

@ -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")
???
}
}