mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-04 06:05:59 +00:00
Allow to manage passwords for a collective
This commit is contained in:
parent
3c93b63c8a
commit
f74624485f
@ -19,6 +19,7 @@ services:
|
||||
image: docspell/joex:latest
|
||||
container_name: docspell-joex
|
||||
command:
|
||||
- -J-Xmx3G
|
||||
- /opt/docspell.conf
|
||||
restart: unless-stopped
|
||||
env_file: ./.env
|
||||
|
@ -63,6 +63,12 @@ trait OCollective[F[_]] {
|
||||
|
||||
def findEnabledSource(sourceId: Ident): F[Option[RSource]]
|
||||
|
||||
def addPassword(collective: Ident, pw: Password): F[Unit]
|
||||
|
||||
def getPasswords(collective: Ident): F[List[RCollectivePassword]]
|
||||
|
||||
def removePassword(id: Ident): F[Unit]
|
||||
|
||||
def startLearnClassifier(collective: Ident): F[Unit]
|
||||
|
||||
def startEmptyTrash(args: EmptyTrashArgs): F[Unit]
|
||||
@ -149,7 +155,7 @@ object OCollective {
|
||||
private def updateLearnClassifierTask(coll: Ident, sett: Settings): F[Unit] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
on = sett.classifier.map(_.enabled).getOrElse(false)
|
||||
on = sett.classifier.exists(_.enabled)
|
||||
timer = sett.classifier.map(_.schedule).getOrElse(CalEvent.unsafe(""))
|
||||
args = LearnClassifierArgs(coll)
|
||||
ut = UserTask(
|
||||
@ -174,6 +180,18 @@ object OCollective {
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def addPassword(collective: Ident, pw: Password): F[Unit] =
|
||||
for {
|
||||
cpass <- RCollectivePassword.createNew[F](collective, pw)
|
||||
_ <- store.transact(RCollectivePassword.upsert(cpass))
|
||||
} yield ()
|
||||
|
||||
def getPasswords(collective: Ident): F[List[RCollectivePassword]] =
|
||||
store.transact(RCollectivePassword.findAll(collective))
|
||||
|
||||
def removePassword(id: Ident): F[Unit] =
|
||||
store.transact(RCollectivePassword.deleteById(id)).map(_ => ())
|
||||
|
||||
def startLearnClassifier(collective: Ident): F[Unit] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
|
@ -5635,6 +5635,7 @@ components:
|
||||
- integrationEnabled
|
||||
- classifier
|
||||
- emptyTrash
|
||||
- passwords
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
@ -5648,6 +5649,11 @@ components:
|
||||
$ref: "#/components/schemas/ClassifierSetting"
|
||||
emptyTrash:
|
||||
$ref: "#/components/schemas/EmptyTrashSetting"
|
||||
passwords:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: password
|
||||
|
||||
EmptyTrashSetting:
|
||||
description: |
|
||||
|
@ -12,8 +12,7 @@ import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OCollective
|
||||
import docspell.common.EmptyTrashArgs
|
||||
import docspell.common.ListType
|
||||
import docspell.common._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s._
|
||||
@ -62,7 +61,8 @@ object CollectiveRoutes {
|
||||
settings.emptyTrash.schedule,
|
||||
settings.emptyTrash.minAge
|
||||
)
|
||||
)
|
||||
),
|
||||
settings.passwords.map(Password.apply)
|
||||
)
|
||||
res <-
|
||||
backend.collective
|
||||
@ -89,7 +89,8 @@ object CollectiveRoutes {
|
||||
EmptyTrashSetting(
|
||||
trash.schedule,
|
||||
trash.minAge
|
||||
)
|
||||
),
|
||||
settDb.map(_.passwords).getOrElse(Nil).map(_.pass)
|
||||
)
|
||||
)
|
||||
resp <- sett.toResponse()
|
||||
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE "collective_password" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"cid" varchar(254) not null,
|
||||
"pass" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE `collective_password` (
|
||||
`id` varchar(254) not null primary key,
|
||||
`cid` varchar(254) not null,
|
||||
`pass` varchar(254) not null,
|
||||
`created` timestamp not null,
|
||||
foreign key (`cid`) references `collective`(`cid`) on delete cascade
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE "collective_password" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"cid" varchar(254) not null,
|
||||
"pass" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
||||
)
|
@ -89,7 +89,8 @@ object RCollective {
|
||||
case None =>
|
||||
REmptyTrashSetting.delete(cid)
|
||||
}
|
||||
} yield n1 + n2 + n3
|
||||
n4 <- RCollectivePassword.replaceAll(cid, settings.passwords)
|
||||
} yield n1 + n2 + n3 + n4
|
||||
|
||||
// this hides categories that have been deleted in the meantime
|
||||
// they are finally removed from the json array once the learn classifier task is run
|
||||
@ -99,10 +100,12 @@ object RCollective {
|
||||
prev <- OptionT.fromOption[ConnectionIO](sett.classifier)
|
||||
cats <- OptionT.liftF(RTag.listCategories(coll))
|
||||
next = prev.copy(categories = prev.categories.intersect(cats))
|
||||
} yield sett.copy(classifier = Some(next))).value
|
||||
pws <- OptionT.liftF(RCollectivePassword.findAll(coll))
|
||||
} yield sett.copy(classifier = Some(next), passwords = pws.map(_.password))).value
|
||||
|
||||
private def getRawSettings(coll: Ident): ConnectionIO[Option[Settings]] = {
|
||||
import RClassifierSetting.stringListMeta
|
||||
|
||||
val c = RCollective.as("c")
|
||||
val cs = RClassifierSetting.as("cs")
|
||||
val es = REmptyTrashSetting.as("es")
|
||||
@ -116,7 +119,8 @@ object RCollective {
|
||||
cs.categories.s,
|
||||
cs.listType.s,
|
||||
es.schedule.s,
|
||||
es.minAge.s
|
||||
es.minAge.s,
|
||||
const(0) //dummy value to load Nil as list of passwords
|
||||
),
|
||||
from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id),
|
||||
c.id === coll
|
||||
@ -170,7 +174,11 @@ object RCollective {
|
||||
language: Language,
|
||||
integrationEnabled: Boolean,
|
||||
classifier: Option[RClassifierSetting.Classifier],
|
||||
emptyTrash: Option[REmptyTrashSetting.EmptyTrash]
|
||||
emptyTrash: Option[REmptyTrashSetting.EmptyTrash],
|
||||
passwords: List[Password]
|
||||
)
|
||||
|
||||
implicit val passwordListMeta: Read[List[Password]] =
|
||||
Read[Int].map(_ => Nil: List[Password])
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import docspell.common._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.qb.DSL._
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
final case class RCollectivePassword(
|
||||
id: Ident,
|
||||
cid: Ident,
|
||||
password: Password,
|
||||
created: Timestamp
|
||||
) {}
|
||||
|
||||
object RCollectivePassword {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName: String = "collective_password"
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val password = Column[Password]("pass", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all: NonEmptyList[Column[_]] =
|
||||
NonEmptyList.of(id, cid, password, created)
|
||||
}
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def createNew[F[_]: Sync](cid: Ident, pw: Password): F[RCollectivePassword] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
time <- Timestamp.current[F]
|
||||
} yield RCollectivePassword(id, cid, pw, time)
|
||||
|
||||
def insert(v: RCollectivePassword): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id}, ${v.cid},${v.password},${v.created}"
|
||||
)
|
||||
|
||||
def upsert(v: RCollectivePassword): ConnectionIO[Int] =
|
||||
for {
|
||||
k <- deleteByPassword(v.cid, v.password)
|
||||
n <- insert(v)
|
||||
} yield n + k
|
||||
|
||||
def deleteById(id: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.id === id)
|
||||
|
||||
def deleteByPassword(cid: Ident, pw: Password): ConnectionIO[Int] =
|
||||
DML.delete(T, T.password === pw && T.cid === cid)
|
||||
|
||||
def findAll(cid: Ident): ConnectionIO[List[RCollectivePassword]] =
|
||||
Select(select(T.all), from(T), T.cid === cid).build
|
||||
.query[RCollectivePassword]
|
||||
.to[List]
|
||||
|
||||
def replaceAll(cid: Ident, pws: List[Password]): ConnectionIO[Int] =
|
||||
for {
|
||||
k <- DML.delete(T, T.cid === cid)
|
||||
pw <- pws.traverse(p => createNew[ConnectionIO](cid, p))
|
||||
n <-
|
||||
if (pws.isEmpty) 0.pure[ConnectionIO]
|
||||
else
|
||||
DML.insertMany(
|
||||
T,
|
||||
T.all,
|
||||
pw.map(p => fr"${p.id},${p.cid},${p.password},${p.created}")
|
||||
)
|
||||
} yield k + n
|
||||
}
|
@ -22,6 +22,7 @@ import Comp.ClassifierSettingsForm
|
||||
import Comp.Dropdown
|
||||
import Comp.EmptyTrashForm
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.StringListInput
|
||||
import Data.DropdownStyle as DS
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.Language exposing (Language)
|
||||
@ -30,6 +31,7 @@ import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onClick, onInput)
|
||||
import Http
|
||||
import Markdown
|
||||
import Messages.Comp.CollectiveSettingsForm exposing (Texts)
|
||||
import Styles as S
|
||||
|
||||
@ -44,6 +46,8 @@ type alias Model =
|
||||
, startClassifierResult : ClassifierResult
|
||||
, emptyTrashModel : Comp.EmptyTrashForm.Model
|
||||
, startEmptyTrashResult : EmptyTrashResult
|
||||
, passwordModel : Comp.StringListInput.Model
|
||||
, passwords : List String
|
||||
}
|
||||
|
||||
|
||||
@ -96,6 +100,8 @@ init flags settings =
|
||||
, startClassifierResult = ClassifierResultInitial
|
||||
, emptyTrashModel = em
|
||||
, startEmptyTrashResult = EmptyTrashResultInitial
|
||||
, passwordModel = Comp.StringListInput.init
|
||||
, passwords = settings.passwords
|
||||
}
|
||||
, Cmd.batch [ Cmd.map ClassifierSettingMsg cc, Cmd.map EmptyTrashMsg ec ]
|
||||
)
|
||||
@ -114,6 +120,7 @@ getSettings model =
|
||||
, integrationEnabled = model.intEnabled
|
||||
, classifier = cls
|
||||
, emptyTrash = trash
|
||||
, passwords = model.passwords
|
||||
}
|
||||
)
|
||||
(Comp.ClassifierSettingsForm.getSettings model.classifierModel)
|
||||
@ -133,6 +140,7 @@ type Msg
|
||||
| StartEmptyTrashTask
|
||||
| StartClassifierResp (Result Http.Error BasicResult)
|
||||
| StartEmptyTrashResp (Result Http.Error BasicResult)
|
||||
| PasswordMsg Comp.StringListInput.Msg
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe CollectiveSettings )
|
||||
@ -285,6 +293,27 @@ update flags msg model =
|
||||
, Nothing
|
||||
)
|
||||
|
||||
PasswordMsg lm ->
|
||||
let
|
||||
( pm, action ) =
|
||||
Comp.StringListInput.update lm model.passwordModel
|
||||
|
||||
pws =
|
||||
case action of
|
||||
Comp.StringListInput.AddAction pw ->
|
||||
pw :: model.passwords
|
||||
|
||||
Comp.StringListInput.RemoveAction pw ->
|
||||
List.filter (\e -> e /= pw) model.passwords
|
||||
|
||||
Comp.StringListInput.NoAction ->
|
||||
model.passwords
|
||||
in
|
||||
( { model | passwordModel = pm, passwords = pws }
|
||||
, Cmd.none
|
||||
, Nothing
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- View2
|
||||
@ -460,6 +489,18 @@ view2 flags texts settings model =
|
||||
]
|
||||
]
|
||||
]
|
||||
, div []
|
||||
[ h2 [ class S.header2 ]
|
||||
[ text texts.passwords
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ div [ class "opacity-50 text-sm" ]
|
||||
[ Markdown.toHtml [] texts.passwordsInfo
|
||||
]
|
||||
, Html.map PasswordMsg
|
||||
(Comp.StringListInput.view2 model.passwords model.passwordModel)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
@ -44,6 +44,8 @@ type alias Texts =
|
||||
, fulltextReindexSubmitted : String
|
||||
, fulltextReindexOkMissing : String
|
||||
, emptyTrash : String
|
||||
, passwords : String
|
||||
, passwordsInfo : String
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +79,8 @@ gb =
|
||||
, fulltextReindexOkMissing =
|
||||
"Please type OK in the field if you really want to start re-indexing your data."
|
||||
, emptyTrash = "Empty Trash"
|
||||
, passwords = "Passwords"
|
||||
, passwordsInfo = "These passwords are used when encrypted PDFs are being processed. Please note, that they are stored in the database as **plain text**!"
|
||||
}
|
||||
|
||||
|
||||
@ -110,4 +114,6 @@ de =
|
||||
, fulltextReindexOkMissing =
|
||||
"Bitte tippe OK in das Feld ein, wenn Du wirklich den Index neu erzeugen möchtest."
|
||||
, emptyTrash = "Papierkorb löschen"
|
||||
, passwords = "Passwörter"
|
||||
, passwordsInfo = "Diese Passwörter werden zum Lesen von verschlüsselten PDFs verwendet. Diese Passwörter werden in der Datanbank **in Klartext** gespeichert!"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user