Add a new column to distinguish local from external users

This commit is contained in:
eikek 2021-09-05 17:08:52 +02:00
parent b73c252762
commit aef56233a5
16 changed files with 120 additions and 11 deletions

View File

@ -254,6 +254,12 @@ val openapiScalaSettings = Seq(
field =>
field
.copy(typeDef = TypeDef("LenientUri", Imports("docspell.common.LenientUri")))
case "accountsource" =>
field =>
field
.copy(typeDef =
TypeDef("AccountSource", Imports("docspell.common.AccountSource"))
)
})
)

View File

@ -194,7 +194,7 @@ object Login {
logF.info(s"Account lookup via remember me: $data")
)
res <- OptionT.liftF(
if (checkNoPassword(data))
if (checkNoPassword(data, AccountSource.all.toList.toSet))
logF.info("RememberMe auth successful") *> okResult(data.account)
else
logF.warn("RememberMe auth not successful") *> Result.invalidAuth.pure[F]
@ -260,13 +260,17 @@ object Login {
private def check(given: String)(data: QLogin.Data): Boolean = {
val passOk = BCrypt.checkpw(given, data.password.pass)
checkNoPassword(data) && passOk
checkNoPassword(data, Set(AccountSource.Local)) && passOk
}
private def checkNoPassword(data: QLogin.Data): Boolean = {
def checkNoPassword(
data: QLogin.Data,
expectedSources: Set[AccountSource]
): Boolean = {
val collOk = data.collectiveState == CollectiveState.Active ||
data.collectiveState == CollectiveState.ReadOnly
val userOk = data.userState == UserState.Active
val userOk =
data.userState == UserState.Active && expectedSources.contains(data.source)
collOk && userOk
}
})

View File

@ -9,7 +9,6 @@ package docspell.backend.ops
import cats.effect.{Async, Resource}
import cats.implicits._
import fs2.Stream
import docspell.backend.JobFactory
import docspell.backend.PasswordCrypt
import docspell.backend.ops.OCollective._
@ -20,7 +19,6 @@ import docspell.store.queue.JobQueue
import docspell.store.records._
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
import docspell.store.{AddResult, Store}
import com.github.eikek.calev._
trait OCollective[F[_]] {
@ -95,9 +93,11 @@ object OCollective {
object PassResetResult {
case class Success(newPw: Password) extends PassResetResult
case object NotFound extends PassResetResult
case object UserNotLocal extends PassResetResult
def success(np: Password): PassResetResult = Success(np)
def notFound: PassResetResult = NotFound
def userNotLocal: PassResetResult = UserNotLocal
}
sealed trait PassChangeResult
@ -105,12 +105,14 @@ object OCollective {
case object UserNotFound extends PassChangeResult
case object PasswordMismatch extends PassChangeResult
case object UpdateFailed extends PassChangeResult
case object UserNotLocal extends PassChangeResult
case object Success extends PassChangeResult
def userNotFound: PassChangeResult = UserNotFound
def passwordMismatch: PassChangeResult = PasswordMismatch
def success: PassChangeResult = Success
def updateFailed: PassChangeResult = UpdateFailed
def userNotLocal: PassChangeResult = UserNotLocal
}
case class RegisterData(
@ -245,11 +247,14 @@ object OCollective {
def resetPassword(accountId: AccountId): F[PassResetResult] =
for {
newPass <- Password.generate[F]
optUser <- store.transact(RUser.findByAccount(accountId))
n <- store.transact(
RUser.updatePassword(accountId, PasswordCrypt.crypt(newPass))
)
res =
if (n <= 0) PassResetResult.notFound
if (optUser.exists(_.source != AccountSource.Local))
PassResetResult.userNotLocal
else if (n <= 0) PassResetResult.notFound
else PassResetResult.success(newPass)
} yield res
@ -270,6 +275,8 @@ object OCollective {
res = check match {
case Some(true) =>
if (n.getOrElse(0) > 0) PassChangeResult.success
else if (optUser.exists(_.source != AccountSource.Local))
PassChangeResult.userNotLocal
else PassChangeResult.updateFailed
case Some(false) =>
PassChangeResult.passwordMismatch

View File

@ -109,6 +109,7 @@ object OSignup {
data.collName,
PasswordCrypt.crypt(data.password),
UserState.Active,
AccountSource.Local,
None,
0,
None,

View File

@ -0,0 +1,35 @@
package docspell.common
import cats.data.NonEmptyList
import io.circe.{Decoder, Encoder}
sealed trait AccountSource { self: Product =>
def name: String =
self.productPrefix.toLowerCase
}
object AccountSource {
case object Local extends AccountSource
case object OpenId extends AccountSource
val all: NonEmptyList[AccountSource] =
NonEmptyList.of(Local, OpenId)
def fromString(str: String): Either[String, AccountSource] =
str.toLowerCase match {
case "local" => Right(Local)
case "openid" => Right(OpenId)
case _ => Left(s"Invalid account source: $str")
}
def unsafeFromString(str: String): AccountSource =
fromString(str).fold(sys.error, identity)
implicit val jsonDecoder: Decoder[AccountSource] =
Decoder.decodeString.emap(fromString)
implicit val jsonEncoder: Encoder[AccountSource] =
Encoder.encodeString.contramap(_.name)
}

View File

@ -5405,6 +5405,7 @@ components:
- id
- login
- state
- source
- loginCount
- created
properties:
@ -5420,6 +5421,12 @@ components:
enum:
- active
- disabled
source:
type: string
format: accountsource
enum:
- local
- openid
password:
type: string
format: password

View File

@ -522,6 +522,7 @@ trait Conversions {
ru.uid,
ru.login,
ru.state,
ru.source,
None,
ru.email,
ru.lastLogin,
@ -537,6 +538,7 @@ trait Conversions {
cid,
u.password.getOrElse(Password.empty),
u.state,
u.source,
u.email,
0,
None,
@ -551,6 +553,7 @@ trait Conversions {
cid,
u.password.getOrElse(Password.empty),
u.state,
u.source,
u.email,
u.loginCount,
u.lastLogin,
@ -706,6 +709,8 @@ 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.")
}
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =

View File

@ -86,6 +86,12 @@ object UserRoutes {
Password(""),
"Password update failed. User not found."
)
case OCollective.PassResetResult.UserNotLocal =>
ResetPasswordResult(
false,
Password(""),
"Password update failed. User is not local, passwords are managed externally."
)
})
} yield resp
}

View File

@ -0,0 +1,8 @@
ALTER TABLE "user_"
ADD COLUMN "account_source" varchar(254);
UPDATE "user_"
SET "account_source" = 'local';
ALTER TABLE "user_"
ALTER COLUMN "account_source" SET NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE `user_`
ADD COLUMN (`account_source` varchar(254));
UPDATE `user_`
SET `account_source` = 'local';
ALTER TABLE `user_`
MODIFY `account_source` varchar(254) NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE "user_"
ADD COLUMN "account_source" varchar(254);
UPDATE "user_"
SET "account_source" = 'local';
ALTER TABLE "user_"
ALTER COLUMN "account_source" SET NOT NULL;

View File

@ -35,6 +35,9 @@ trait DoobieMeta extends EmilDoobieMeta {
e.apply(a).noSpaces
)
implicit val metaAccountSource: Meta[AccountSource] =
Meta[String].imap(AccountSource.unsafeFromString)(_.name)
implicit val metaDuration: Meta[Duration] =
Meta[Long].imap(Duration.millis)(_.millis)

View File

@ -24,7 +24,8 @@ object QLogin {
account: AccountId,
password: Password,
collectiveState: CollectiveState,
userState: UserState
userState: UserState,
source: AccountSource
)
def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
@ -32,7 +33,7 @@ object QLogin {
val coll = RCollective.as("c")
val sql =
Select(
select(user.cid, user.login, user.password, coll.state, user.state),
select(user.cid, user.login, user.password, coll.state, user.state, user.source),
from(user).innerJoin(coll, user.cid === coll.id),
user.login === acc.user && user.cid === acc.collective
).build

View File

@ -21,6 +21,7 @@ case class RUser(
cid: Ident,
password: Password,
state: UserState,
source: AccountSource,
email: Option[String],
loginCount: Int,
lastLogin: Option[Timestamp],
@ -36,6 +37,7 @@ object RUser {
val cid = Column[Ident]("cid", this)
val password = Column[Password]("password", this)
val state = Column[UserState]("state", this)
val source = Column[AccountSource]("account_source", this)
val email = Column[String]("email", this)
val loginCount = Column[Int]("logincount", this)
val lastLogin = Column[Timestamp]("lastlogin", this)
@ -48,6 +50,7 @@ object RUser {
cid,
password,
state,
source,
email,
loginCount,
lastLogin,
@ -65,7 +68,7 @@ object RUser {
DML.insert(
t,
t.all,
fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.source},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
)
}
@ -134,7 +137,7 @@ object RUser {
val t = Table(None)
DML.update(
t,
t.cid === accountId.collective && t.login === accountId.user,
t.cid === accountId.collective && t.login === accountId.user && t.source === AccountSource.Local,
DML.set(t.password.setTo(hashedPass))
)
}

View File

@ -66,6 +66,7 @@ view2 texts model =
[ th [ class "w-px whitespace-nowrap" ] []
, th [ class "text-left" ] [ text texts.login ]
, th [ class "text-center" ] [ text texts.state ]
, th [ class "text-center" ] [ text texts.source ]
, th [ class "hidden md:table-cell text-left" ] [ text texts.email ]
, th [ class "hidden md:table-cell text-center" ] [ text texts.logins ]
, th [ class "hidden sm:table-cell text-center" ] [ text texts.lastLogin ]
@ -92,6 +93,9 @@ renderUserLine2 texts model user =
, td [ class "text-center" ]
[ text user.state
]
, td [ class "text-center" ]
[ text user.source
]
, td [ class "hidden md:table-cell text-left" ]
[ Maybe.withDefault "" user.email |> text
]

View File

@ -20,6 +20,7 @@ type alias Texts =
{ basics : Messages.Basics.Texts
, login : String
, state : String
, source : String
, email : String
, logins : String
, lastLogin : String
@ -32,6 +33,7 @@ gb =
{ basics = Messages.Basics.gb
, login = "Login"
, state = "State"
, source = "Type"
, email = "E-Mail"
, logins = "Logins"
, lastLogin = "Last Login"
@ -44,6 +46,7 @@ de =
{ basics = Messages.Basics.de
, login = "Benutzername"
, state = "Status"
, source = "Typ"
, email = "E-Mail"
, logins = "Anmeldungen"
, lastLogin = "Letzte Anmeldung"