Adopt store module to new collective table

This commit is contained in:
eikek
2022-07-05 21:17:18 +02:00
parent 35882fce84
commit 77f22bb5ea
65 changed files with 783 additions and 635 deletions

View File

@ -18,12 +18,13 @@ import scodec.bits.ByteVector
case class AuthToken(
nowMillis: Long,
account: AccountId,
account: AccountInfo,
requireSecondFactor: Boolean,
valid: Option[Duration],
salt: String,
sig: String
) {
def asString =
valid match {
case Some(v) =>
@ -63,7 +64,7 @@ object AuthToken {
for {
millis <- TokenUtil.asInt(ms).toRight("Cannot read authenticator data")
acc <- TokenUtil.b64dec(as).toRight("Cannot read authenticator data")
accId <- AccountId.parse(acc)
accId <- AccountInfo.parse(acc)
twofac <- Right[String, Boolean](java.lang.Boolean.parseBoolean(fa))
valid <- TokenUtil
.asInt(vs)
@ -75,7 +76,7 @@ object AuthToken {
for {
millis <- TokenUtil.asInt(ms).toRight("Cannot read authenticator data")
acc <- TokenUtil.b64dec(as).toRight("Cannot read authenticator data")
accId <- AccountId.parse(acc)
accId <- AccountInfo.parse(acc)
twofac <- Right[String, Boolean](java.lang.Boolean.parseBoolean(fa))
} yield AuthToken(millis, accId, twofac, None, salt, sig)
@ -84,7 +85,7 @@ object AuthToken {
}
def user[F[_]: Sync](
accountId: AccountId,
accountId: AccountInfo,
requireSecondFactor: Boolean,
key: ByteVector,
valid: Option[Duration]

View File

@ -96,10 +96,12 @@ object Login {
for {
data <- store.transact(QLogin.findUser(accountId))
_ <- logF.trace(s"Account lookup: $data")
res <-
if (data.exists(checkNoPassword(_, Set(AccountSource.OpenId))))
doLogin(config, accountId, false)
else Result.invalidAuth.pure[F]
res <- data match {
case Some(d) if checkNoPassword(d, Set(AccountSource.OpenId)) =>
doLogin(config, d.account, false)
case _ =>
Result.invalidAuth.pure[F]
}
} yield res
def loginSession(config: Config)(sessionKey: String): F[Result] =
@ -122,9 +124,12 @@ object Login {
for {
data <- store.transact(QLogin.findUser(acc))
_ <- logF.trace(s"Account lookup: $data")
res <-
if (data.exists(check(up.pass))) doLogin(config, acc, up.rememberMe)
else Result.invalidAuth.pure[F]
res <- data match {
case Some(d) if check(up.pass)(d) =>
doLogin(config, d.account, up.rememberMe)
case _ =>
Result.invalidAuth.pure[F]
}
} yield res
case Left(_) =>
logF.info(s"User authentication failed for: ${up.hidePass}") *>
@ -162,7 +167,7 @@ object Login {
(for {
_ <- validateToken
key <- EitherT.fromOptionF(
store.transact(RTotp.findEnabledByLogin(sf.token.account, true)),
store.transact(RTotp.findEnabledByLogin(sf.token.account.userId, true)),
Result.invalidAuth
)
now <- EitherT.right[Result](Timestamp.current[F])
@ -175,13 +180,13 @@ object Login {
}
def loginRememberMe(config: Config)(token: String): F[Result] = {
def okResult(acc: AccountId) =
def okResult(acc: AccountInfo) =
for {
_ <- store.transact(RUser.updateLogin(acc))
token <- AuthToken.user(acc, false, config.serverSecret, None)
} yield Result.ok(token, None)
def doLogin(rid: Ident) =
def rememberedLogin(rid: Ident) =
(for {
now <- OptionT.liftF(Timestamp.current[F])
minTime = now - config.rememberMe.valid
@ -214,7 +219,7 @@ object Login {
else if (rt.isExpired(config.rememberMe.valid))
logF.info(s"RememberMe cookie expired ($rt).") *> Result.invalidTime
.pure[F]
else doLogin(rt.rememberId)
else rememberedLogin(rt.rememberId)
case Left(err) =>
logF.info(s"RememberMe cookie was invalid: $err") *> Result.invalidAuth
.pure[F]
@ -245,11 +250,11 @@ object Login {
private def doLogin(
config: Config,
acc: AccountId,
acc: AccountInfo,
rememberMe: Boolean
): F[Result] =
for {
require2FA <- store.transact(RTotp.isEnabled(acc))
require2FA <- store.transact(RTotp.isEnabled(acc.userId))
_ <-
if (require2FA) ().pure[F]
else store.transact(RUser.updateLogin(acc))
@ -263,13 +268,11 @@ object Login {
private def insertRememberToken(
store: Store[F],
acc: AccountId,
acc: AccountInfo,
config: Config
): F[RememberToken] =
for {
uid <- OptionT(store.transact(RUser.findIdByAccount(acc)))
.getOrRaise(new IllegalStateException(s"No user_id found for account: $acc"))
rme <- RRememberMe.generate[F](uid)
rme <- RRememberMe.generate[F](acc.userId)
_ <- store.transact(RRememberMe.insert(rme))
token <- RememberToken.user(rme.id, config.serverSecret)
} yield token

View File

@ -6,7 +6,6 @@
package docspell.backend.ops
import cats.data.OptionT
import cats.effect._
import cats.implicits._
@ -19,19 +18,19 @@ import docspell.store.records._
trait OQueryBookmarks[F[_]] {
def getAll(account: AccountId): F[Vector[OQueryBookmarks.Bookmark]]
def getAll(account: AccountInfo): F[Vector[OQueryBookmarks.Bookmark]]
def findOne(account: AccountId, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]]
def findOne(account: AccountInfo, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]]
def create(account: AccountId, bookmark: OQueryBookmarks.NewBookmark): F[AddResult]
def create(account: AccountInfo, bookmark: OQueryBookmarks.NewBookmark): F[AddResult]
def update(
account: AccountId,
account: AccountInfo,
id: Ident,
bookmark: OQueryBookmarks.NewBookmark
): F[UpdateResult]
def delete(account: AccountId, bookmark: Ident): F[Unit]
def delete(account: AccountInfo, bookmark: Ident): F[Unit]
}
object OQueryBookmarks {
@ -53,39 +52,43 @@ object OQueryBookmarks {
def apply[F[_]: Sync](store: Store[F]): Resource[F, OQueryBookmarks[F]] =
Resource.pure(new OQueryBookmarks[F] {
def getAll(account: AccountId): F[Vector[Bookmark]] =
def getAll(account: AccountInfo): F[Vector[Bookmark]] =
store
.transact(RQueryBookmark.allForUser(account))
.transact(RQueryBookmark.allForUser(account.collectiveId, account.userId))
.map(_.map(convert.toModel))
def findOne(
account: AccountId,
account: AccountInfo,
nameOrId: String
): F[Option[OQueryBookmarks.Bookmark]] =
store
.transact(RQueryBookmark.findByNameOrId(account, nameOrId))
.transact(
RQueryBookmark.findByNameOrId(account.collectiveId, account.userId, nameOrId)
)
.map(_.map(convert.toModel))
def create(account: AccountId, b: NewBookmark): F[AddResult] = {
def create(account: AccountInfo, b: NewBookmark): F[AddResult] = {
val uid = if (b.personal) account.userId.some else None
val record =
RQueryBookmark.createNew(account, b.name, b.label, b.query, b.personal)
store.transact(RQueryBookmark.insertIfNotExists(account, record))
RQueryBookmark.createNew(
account.collectiveId,
uid,
b.name,
b.label,
b.query
)
store.transact(
RQueryBookmark.insertIfNotExists(account.collectiveId, account.userId, record)
)
}
def update(account: AccountId, id: Ident, b: NewBookmark): F[UpdateResult] =
def update(acc: AccountInfo, id: Ident, b: NewBookmark): F[UpdateResult] =
UpdateResult.fromUpdate(
store.transact {
(for {
userId <- OptionT(RUser.findIdByAccount(account))
n <- OptionT.liftF(
RQueryBookmark.update(convert.toRecord(account, id, userId, b))
)
} yield n).getOrElse(0)
}
store.transact(RQueryBookmark.update(convert.toRecord(acc, id, b)))
)
def delete(account: AccountId, bookmark: Ident): F[Unit] =
store.transact(RQueryBookmark.deleteById(account.collective, bookmark)).as(())
def delete(account: AccountInfo, bookmark: Ident): F[Unit] =
store.transact(RQueryBookmark.deleteById(account.collectiveId, bookmark)).as(())
})
private object convert {
@ -94,17 +97,16 @@ object OQueryBookmarks {
Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created)
def toRecord(
account: AccountId,
account: AccountInfo,
id: Ident,
userId: Ident,
b: NewBookmark
): RQueryBookmark =
RQueryBookmark(
id,
b.name,
b.label,
if (b.personal) userId.some else None,
account.collective,
if (b.personal) account.userId.some else None,
account.collectiveId,
b.query,
Timestamp.Epoch
)

View File

@ -6,29 +6,29 @@
package docspell.backend.ops
import cats.data.OptionT
import cats.effect._
import cats.implicits._
import docspell.backend.ops.OTotp.{ConfirmResult, InitResult, OtpState}
import docspell.common._
import docspell.store.records.{RTotp, RUser}
import docspell.store.records.RTotp
import docspell.store.{AddResult, Store, UpdateResult}
import docspell.totp.{Key, OnetimePassword, Totp}
trait OTotp[F[_]] {
/** Return whether TOTP is enabled for this account or not. */
def state(accountId: AccountId): F[OtpState]
def state(accountId: AccountInfo): F[OtpState]
/** Initializes TOTP by generating a secret and storing it in the database. TOTP is
* still disabled, it must be confirmed in order to be active.
*/
def initialize(accountId: AccountId): F[InitResult]
def initialize(accountId: AccountInfo): F[InitResult]
/** Confirms and finishes initialization. TOTP is active after this for the given
* account.
*/
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult]
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult]
/** Disables TOTP and removes the shared secret. If a otp is specified, it must be
* valid.
@ -57,7 +57,7 @@ object OTotp {
sealed trait InitResult
object InitResult {
final case class Success(accountId: AccountId, key: Key) extends InitResult {
final case class Success(accountId: AccountInfo, key: Key) extends InitResult {
def authenticatorUrl(issuer: String): LenientUri =
LenientUri.unsafe(
s"otpauth://totp/$issuer:${accountId.asString}?secret=${key.data.toBase32}&issuer=$issuer"
@ -67,7 +67,7 @@ object OTotp {
case object NotFound extends InitResult
final case class Failed(ex: Throwable) extends InitResult
def success(accountId: AccountId, key: Key): InitResult =
def success(accountId: AccountInfo, key: Key): InitResult =
Success(accountId, key)
def alreadyExists: InitResult = AlreadyExists
@ -85,47 +85,41 @@ object OTotp {
Resource.pure[F, OTotp[F]](new OTotp[F] {
val log = docspell.logging.getLogger[F]
def initialize(accountId: AccountId): F[InitResult] =
def initialize(accountId: AccountInfo): F[InitResult] =
for {
_ <- log.info(s"Initializing TOTP for account ${accountId.asString}")
userId <- store.transact(RUser.findIdByAccount(accountId))
result <- userId match {
case Some(uid) =>
for {
record <- RTotp.generate[F](uid, totp.settings.mac)
un <- store.transact(RTotp.updateDisabled(record))
an <-
if (un != 0)
AddResult.entityExists("Entity exists, but update was ok").pure[F]
else store.add(RTotp.insert(record), RTotp.existsByLogin(accountId))
innerResult <-
if (un != 0) InitResult.success(accountId, record.secret).pure[F]
else
an match {
case AddResult.EntityExists(msg) =>
log.warn(
s"A totp record already exists for account '${accountId.asString}': $msg!"
) *>
InitResult.alreadyExists.pure[F]
case AddResult.Failure(ex) =>
log.warn(
s"Failed to setup totp record for '${accountId.asString}': ${ex.getMessage}"
) *>
InitResult.failed(ex).pure[F]
case AddResult.Success =>
InitResult.success(accountId, record.secret).pure[F]
}
} yield innerResult
case None =>
log.warn(s"No user found for account: ${accountId.asString}!") *>
InitResult.NotFound.pure[F]
}
result <- for {
record <- RTotp.generate[F](accountId.userId, totp.settings.mac)
un <- store.transact(RTotp.updateDisabled(record))
an <-
if (un != 0)
AddResult.entityExists("Entity exists, but update was ok").pure[F]
else store.add(RTotp.insert(record), RTotp.existsByUserId(accountId.userId))
innerResult <-
if (un != 0) InitResult.success(accountId, record.secret).pure[F]
else
an match {
case AddResult.EntityExists(msg) =>
log.warn(
s"A totp record already exists for account '${accountId.asString}': $msg!"
) *>
InitResult.alreadyExists.pure[F]
case AddResult.Failure(ex) =>
log.warn(
s"Failed to setup totp record for '${accountId.asString}': ${ex.getMessage}"
) *>
InitResult.failed(ex).pure[F]
case AddResult.Success =>
InitResult.success(accountId, record.secret).pure[F]
}
} yield innerResult
} yield result
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult] =
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult] =
for {
_ <- log.info(s"Confirm TOTP setup for account ${accountId.asString}")
key <- store.transact(RTotp.findEnabledByLogin(accountId, false))
key <- store.transact(RTotp.findEnabledByUserId(accountId.userId, false))
now <- Timestamp.current[F]
res <- key match {
case None =>
@ -134,7 +128,7 @@ object OTotp {
val check = totp.checkPassword(r.secret, otp, now.value)
if (check)
store
.transact(RTotp.setEnabled(accountId, true))
.transact(RTotp.setEnabled(accountId.userId, true))
.map(_ => ConfirmResult.Success)
else ConfirmResult.Failed.pure[F]
}
@ -154,7 +148,7 @@ object OTotp {
val check = totp.checkPassword(r.secret, pw, now.value)
if (check)
UpdateResult.fromUpdate(
store.transact(RTotp.setEnabled(accountId, false))
store.transact(RTotp.setEnabled(r.userId, false))
)
else
log.info(s"TOTP code was invalid. Not disabling it.") *> UpdateResult
@ -163,12 +157,17 @@ object OTotp {
}
} yield res
case None =>
UpdateResult.fromUpdate(store.transact(RTotp.setEnabled(accountId, false)))
UpdateResult.fromUpdate {
(for {
key <- OptionT(RTotp.findEnabledByLogin(accountId, true))
n <- OptionT.liftF(RTotp.setEnabled(key.userId, false))
} yield n).mapK(store.transform).getOrElse(0)
}
}
def state(accountId: AccountId): F[OtpState] =
def state(acc: AccountInfo): F[OtpState] =
for {
record <- store.transact(RTotp.findEnabledByLogin(accountId, true))
record <- store.transact(RTotp.findEnabledByUserId(acc.userId, true))
result = record match {
case Some(r) =>
OtpState.Enabled(r.created)