mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Allow to authenticate with the same account from different sources
A new config allows to treat an account same independent where it was created (openid or local). Issue: #1827 #1781
This commit is contained in:
@ -95,6 +95,17 @@ docspell.server {
|
||||
# How long the remember me cookie/token is valid.
|
||||
valid = "30 days"
|
||||
}
|
||||
|
||||
# One of: fail, convert
|
||||
#
|
||||
# Accounts can be local or defined at a remote provider and
|
||||
# integrated via OIDC. If the same account is defined in both
|
||||
# sources, docspell by default fails if a user mixes logins (e.g.
|
||||
# when registering a user locally and then logging in with the
|
||||
# same user via OIDC). When set to `convert` docspell treats it as
|
||||
# being the same and simply updates the account to reflect the new
|
||||
# account source.
|
||||
on-account-source-conflict = "fail"
|
||||
}
|
||||
|
||||
# Settings for "download as zip"
|
||||
|
@ -11,6 +11,7 @@ import java.security.SecureRandom
|
||||
import cats.Monoid
|
||||
import cats.effect.Async
|
||||
|
||||
import docspell.backend.auth.Login
|
||||
import docspell.backend.signup.{Config => SignupConfig}
|
||||
import docspell.config.Implicits._
|
||||
import docspell.config.{ConfigFactory, FtsType, Validation}
|
||||
@ -50,6 +51,10 @@ object ConfigFile {
|
||||
|
||||
implicit val openIdExtractorReader: ConfigReader[OpenId.UserInfo.Extractor] =
|
||||
ConfigReader[String].emap(reason(OpenId.UserInfo.Extractor.fromString))
|
||||
|
||||
implicit val onAccountSourceConflictReader
|
||||
: ConfigReader[Login.OnAccountSourceConflict] =
|
||||
ConfigReader[String].emap(reason(Login.OnAccountSourceConflict.fromString))
|
||||
}
|
||||
|
||||
def generateSecretIfEmpty: Validation[Config] =
|
||||
|
@ -81,7 +81,7 @@ final class RestAppImpl[F[_]: Async](
|
||||
Router(
|
||||
"fts" -> FullTextIndexRoutes.admin(config, backend),
|
||||
"user/otp" -> TotpRoutes.admin(backend),
|
||||
"user" -> UserRoutes.admin(backend),
|
||||
"user" -> UserRoutes.admin(backend, config.auth),
|
||||
"info" -> InfoRoutes.admin(config),
|
||||
"attachments" -> AttachmentRoutes.admin(backend),
|
||||
"files" -> FileRepositoryRoutes.admin(backend)
|
||||
@ -129,7 +129,7 @@ final class RestAppImpl[F[_]: Async](
|
||||
"person" -> PersonRoutes(backend, token),
|
||||
"source" -> SourceRoutes(backend, token),
|
||||
"user/otp" -> TotpRoutes(backend, config, token),
|
||||
"user" -> UserRoutes(backend, token),
|
||||
"user" -> UserRoutes(backend, config.auth, token),
|
||||
"collective" -> CollectiveRoutes(backend, token),
|
||||
"queue" -> JobQueueRoutes(backend, token),
|
||||
"item" -> ItemRoutes(config, backend, token),
|
||||
|
@ -116,6 +116,7 @@ object RestServer {
|
||||
)(
|
||||
wsB: WebSocketBuilder2[F]
|
||||
) = {
|
||||
val logger = docspell.logging.getLogger[F]
|
||||
val internal = Router(
|
||||
"/" -> redirectTo("/app"),
|
||||
"/internal" -> InternalHeader(internSettings.internalRouteKey) {
|
||||
@ -123,6 +124,17 @@ object RestServer {
|
||||
}
|
||||
)
|
||||
val httpApp = (internal <+> restApp.routes(wsB)).orNotFound
|
||||
.mapF(
|
||||
_.attempt
|
||||
.flatMap { eab =>
|
||||
eab.fold(
|
||||
ex =>
|
||||
logger.error(ex)("Processing the request resulted in an error.").as(eab),
|
||||
_ => eab.pure[F]
|
||||
)
|
||||
}
|
||||
.rethrow
|
||||
)
|
||||
Logger.httpApp(logHeaders = false, logBody = false)(httpApp)
|
||||
}
|
||||
|
||||
|
@ -699,8 +699,11 @@ trait Conversions {
|
||||
case PassChangeResult.PasswordMismatch =>
|
||||
BasicResult(false, "The current password is incorrect.")
|
||||
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||
case PassChangeResult.UserNotLocal =>
|
||||
BasicResult(false, "User is not local, passwords are managed externally.")
|
||||
case PassChangeResult.InvalidSource(source) =>
|
||||
BasicResult(
|
||||
false,
|
||||
s"User has invalid soure: $source. Passwords are managed elsewhere."
|
||||
)
|
||||
}
|
||||
|
||||
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =
|
||||
|
@ -10,7 +10,8 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.auth.Login.OnAccountSourceConflict
|
||||
import docspell.backend.auth.{AuthToken, Login}
|
||||
import docspell.backend.ops.OCollective
|
||||
import docspell.common._
|
||||
import docspell.restapi.model._
|
||||
@ -24,7 +25,11 @@ import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object UserRoutes {
|
||||
|
||||
def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||
def apply[F[_]: Async](
|
||||
backend: BackendApp[F],
|
||||
loginConfig: Login.Config,
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
@ -36,7 +41,8 @@ object UserRoutes {
|
||||
user.account.collectiveId,
|
||||
user.account.userId,
|
||||
data.currentPassword,
|
||||
data.newPassword
|
||||
data.newPassword,
|
||||
expectedAccountSources(loginConfig)
|
||||
)
|
||||
resp <- Ok(basicResult(res))
|
||||
} yield resp
|
||||
@ -91,14 +97,20 @@ object UserRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
def admin[F[_]: Async](backend: BackendApp[F]): HttpRoutes[F] = {
|
||||
def admin[F[_]: Async](
|
||||
backend: BackendApp[F],
|
||||
loginConfig: Login.Config
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of { case req @ POST -> Root / "resetPassword" =>
|
||||
for {
|
||||
input <- req.as[ResetPassword]
|
||||
result <- backend.collective.resetPassword(input.account)
|
||||
result <- backend.collective.resetPassword(
|
||||
input.account,
|
||||
expectedAccountSources(loginConfig)
|
||||
)
|
||||
resp <- Ok(result match {
|
||||
case OCollective.PassResetResult.Success(np) =>
|
||||
ResetPasswordResult(true, np, "Password updated")
|
||||
@ -108,14 +120,20 @@ object UserRoutes {
|
||||
Password(""),
|
||||
"Password update failed. User not found."
|
||||
)
|
||||
case OCollective.PassResetResult.UserNotLocal =>
|
||||
case OCollective.PassResetResult.InvalidSource(source) =>
|
||||
ResetPasswordResult(
|
||||
false,
|
||||
Password(""),
|
||||
"Password update failed. User is not local, passwords are managed externally."
|
||||
s"Password update failed. User has unexpected source: $source. Passwords are managed externally."
|
||||
)
|
||||
})
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
private def expectedAccountSources(loginConfig: Login.Config): Set[AccountSource] =
|
||||
loginConfig.onAccountSourceConflict match {
|
||||
case OnAccountSourceConflict.Fail => Set(AccountSource.Local)
|
||||
case OnAccountSourceConflict.Convert => AccountSource.all.toList.toSet
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user