From 2ac0b84e5226c2e018852cd648d67ae28f6e612b Mon Sep 17 00:00:00 2001
From: eikek <eike.kettner@posteo.de>
Date: Sat, 23 Oct 2021 23:29:36 +0200
Subject: [PATCH] Link shares to the user, not the collective

The user is required when searching because of folders (sadly), so the
share is connected to the user.
---
 .../scala/docspell/backend/ops/OShare.scala   | 68 ++++++++-----
 .../scala/docspell/common/AccountId.scala     |  1 +
 .../src/main/resources/docspell-openapi.yml   | 13 +++
 .../restserver/http4s/QueryParam.scala        |  3 +-
 .../restserver/routes/ShareRoutes.scala       | 44 +++++----
 .../restserver/routes/ShareSearchRoutes.scala |  2 +-
 .../db/migration/h2/V1.27.1__item_share.sql   |  4 +-
 .../migration/mariadb/V1.27.1__item_share.sql |  4 +-
 .../postgresql/V1.27.1__item_share.sql        |  4 +-
 .../scala/docspell/store/records/RShare.scala | 76 ++++++++++-----
 .../scala/docspell/store/records/RUser.scala  |  8 +-
 modules/webapp/package-lock.json              | 23 +----
 modules/webapp/src/main/elm/Api.elm           | 15 ++-
 .../webapp/src/main/elm/Comp/ShareManage.elm  | 96 ++++++++++++++++---
 .../webapp/src/main/elm/Comp/ShareTable.elm   |  8 +-
 .../main/elm/Messages/Comp/ShareManage.elm    |  6 ++
 .../src/main/elm/Messages/Comp/ShareTable.elm |  3 +
 17 files changed, 268 insertions(+), 110 deletions(-)

diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala
index e8dae28f..ba27ea70 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala
@@ -21,20 +21,24 @@ import docspell.query.ItemQuery.Expr
 import docspell.query.ItemQuery.Expr.AttachId
 import docspell.store.Store
 import docspell.store.queries.SearchSummary
-import docspell.store.records.{RShare, RUserEmail}
+import docspell.store.records._
 
 import emil._
 import scodec.bits.ByteVector
 
 trait OShare[F[_]] {
 
-  def findAll(collective: Ident): F[List[RShare]]
+  def findAll(
+      collective: Ident,
+      ownerLogin: Option[Ident],
+      query: Option[String]
+  ): F[List[ShareData]]
 
   def delete(id: Ident, collective: Ident): F[Boolean]
 
   def addNew(share: OShare.NewShare): F[OShare.ChangeResult]
 
-  def findOne(id: Ident, collective: Ident): OptionT[F, RShare]
+  def findOne(id: Ident, collective: Ident): OptionT[F, ShareData]
 
   def update(
       id: Ident,
@@ -91,12 +95,7 @@ object OShare {
     case object NotFound extends SendResult
   }
 
-  final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) {
-
-    //TODO
-    def asAccount: AccountId =
-      AccountId(cid, Ident.unsafe(""))
-  }
+  final case class ShareQuery(id: Ident, account: AccountId, query: ItemQuery)
 
   sealed trait VerifyResult {
     def toEither: Either[String, ShareToken] =
@@ -121,7 +120,7 @@ object OShare {
   }
 
   final case class NewShare(
-      cid: Ident,
+      account: AccountId,
       name: Option[String],
       query: ItemQuery,
       enabled: Boolean,
@@ -133,11 +132,15 @@ object OShare {
   object ChangeResult {
     final case class Success(id: Ident) extends ChangeResult
     case object PublishUntilInPast extends ChangeResult
+    case object NotFound extends ChangeResult
 
     def success(id: Ident): ChangeResult = Success(id)
     def publishUntilInPast: ChangeResult = PublishUntilInPast
+    def notFound: ChangeResult = NotFound
   }
 
+  final case class ShareData(share: RShare, user: RUser)
+
   def apply[F[_]: Async](
       store: Store[F],
       itemSearch: OItemSearch[F],
@@ -147,8 +150,14 @@ object OShare {
     new OShare[F] {
       private[this] val logger = Logger.log4s[F](org.log4s.getLogger)
 
-      def findAll(collective: Ident): F[List[RShare]] =
-        store.transact(RShare.findAllByCollective(collective))
+      def findAll(
+          collective: Ident,
+          ownerLogin: Option[Ident],
+          query: Option[String]
+      ): F[List[ShareData]] =
+        store
+          .transact(RShare.findAllByCollective(collective, ownerLogin, query))
+          .map(_.map(ShareData.tupled))
 
       def delete(id: Ident, collective: Ident): F[Boolean] =
         store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0)
@@ -157,10 +166,11 @@ object OShare {
         for {
           curTime <- Timestamp.current[F]
           id <- Ident.randomId[F]
+          user <- store.transact(RUser.findByAccount(share.account))
           pass = share.password.map(PasswordCrypt.crypt)
           record = RShare(
             id,
-            share.cid,
+            user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")),
             share.name,
             share.query,
             share.enabled,
@@ -182,9 +192,10 @@ object OShare {
       ): F[ChangeResult] =
         for {
           curTime <- Timestamp.current[F]
+          user <- store.transact(RUser.findByAccount(share.account))
           record = RShare(
             id,
-            share.cid,
+            user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")),
             share.name,
             share.query,
             share.enabled,
@@ -199,11 +210,14 @@ object OShare {
             else
               store
                 .transact(RShare.updateData(record, removePassword))
-                .map(_ => ChangeResult.success(id))
+                .map(n => if (n > 0) ChangeResult.success(id) else ChangeResult.notFound)
         } yield res
 
-      def findOne(id: Ident, collective: Ident): OptionT[F, RShare] =
-        RShare.findOne(id, collective).mapK(store.transform)
+      def findOne(id: Ident, collective: Ident): OptionT[F, ShareData] =
+        RShare
+          .findOne(id, collective)
+          .mapK(store.transform)
+          .map(ShareData.tupled)
 
       def verify(
           key: ByteVector
@@ -211,7 +225,7 @@ object OShare {
         RShare
           .findCurrentActive(id)
           .mapK(store.transform)
-          .semiflatMap { share =>
+          .semiflatMap { case (share, _) =>
             val pwCheck =
               share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw)))
 
@@ -257,7 +271,9 @@ object OShare {
         RShare
           .findCurrentActive(id)
           .mapK(store.transform)
-          .map(share => ShareQuery(share.id, share.cid, share.query))
+          .map { case (share, user) =>
+            ShareQuery(share.id, user.accountId, share.query)
+          }
 
       def findAttachmentPreview(
           attachId: Ident,
@@ -266,21 +282,23 @@ object OShare {
         for {
           sq <- findShareQuery(shareId)
           _ <- checkAttachment(sq, AttachId(attachId.id))
-          res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid))
+          res <- OptionT(
+            itemSearch.findAttachmentPreview(attachId, sq.account.collective)
+          )
         } yield res
 
       def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] =
         for {
           sq <- findShareQuery(shareId)
           _ <- checkAttachment(sq, AttachId(attachId.id))
-          res <- OptionT(itemSearch.findAttachment(attachId, sq.cid))
+          res <- OptionT(itemSearch.findAttachment(attachId, sq.account.collective))
         } yield res
 
       def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] =
         for {
           sq <- findShareQuery(shareId)
           _ <- checkAttachment(sq, Expr.itemIdEq(itemId.id))
-          res <- OptionT(itemSearch.findItem(itemId, sq.cid))
+          res <- OptionT(itemSearch.findItem(itemId, sq.account.collective))
         } yield res
 
       /** Check whether the attachment with the given id is in the results of the given
@@ -288,7 +306,7 @@ object OShare {
         */
       private def checkAttachment(sq: ShareQuery, idExpr: Expr): OptionT[F, Unit] = {
         val checkQuery = Query(
-          Query.Fix(sq.asAccount, Some(sq.query.expr), None),
+          Query.Fix(sq.account, Some(sq.query.expr), None),
           Query.QueryExpr(idExpr)
         )
         OptionT(
@@ -310,7 +328,7 @@ object OShare {
       ): OptionT[F, StringSearchResult[SearchSummary]] =
         findShareQuery(shareId)
           .semiflatMap { share =>
-            val fix = Query.Fix(share.asAccount, Some(share.query.expr), None)
+            val fix = Query.Fix(share.account, Some(share.query.expr), None)
             simpleSearch
               .searchSummaryByString(settings)(fix, q)
               .map {
@@ -350,7 +368,7 @@ object OShare {
         (for {
           _ <- RShare
             .findCurrentActive(mail.shareId)
-            .filter(_.cid == account.collective)
+            .filter(_._2.cid == account.collective)
             .mapK(store.transform)
           mailCfg <- getSmtpSettings
           mail <- createMail(mailCfg)
diff --git a/modules/common/src/main/scala/docspell/common/AccountId.scala b/modules/common/src/main/scala/docspell/common/AccountId.scala
index bd8f1fb1..e8479966 100644
--- a/modules/common/src/main/scala/docspell/common/AccountId.scala
+++ b/modules/common/src/main/scala/docspell/common/AccountId.scala
@@ -8,6 +8,7 @@ package docspell.common
 
 import io.circe._
 
+/** The collective and user name. */
 case class AccountId(collective: Ident, user: Ident) {
   def asString =
     if (collective == user) user.id
diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml
index e5ce7c9e..f7e250fa 100644
--- a/modules/restapi/src/main/resources/docspell-openapi.yml
+++ b/modules/restapi/src/main/resources/docspell-openapi.yml
@@ -1932,6 +1932,9 @@ paths:
         Return a list of all shares for this collective.
       security:
         - authTokenHeader: []
+      parameters:
+        - $ref: "#/components/parameters/q"
+        - $ref: "#/components/parameters/owningShare"
       responses:
         200:
           description: Ok
@@ -4496,6 +4499,7 @@ components:
       required:
         - id
         - query
+        - owner
         - enabled
         - publishAt
         - publishUntil
@@ -4509,6 +4513,8 @@ components:
         query:
           type: string
           format: itemquery
+        owner:
+          $ref: "#/components/schemas/IdName"          
         name:
           type: string
         enabled:
@@ -6805,6 +6811,13 @@ components:
       required: false
       schema:
         type: boolean
+    owningShare:
+      name: owning
+      in: query
+      description: Return my own shares only
+      required: false
+      schema:
+        type: boolean
     checksum:
       name: checksum
       in: path
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala
index 102325da..041814cf 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala
@@ -16,7 +16,7 @@ import docspell.common.SearchMode
 
 import org.http4s.ParseFailure
 import org.http4s.QueryParamDecoder
-import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
+import org.http4s.dsl.impl.{FlagQueryParamMatcher, OptionalQueryParamDecoderMatcher}
 
 object QueryParam {
   case class QueryString(q: String)
@@ -67,6 +67,7 @@ object QueryParam {
   object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
 
   object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
+  object OwningFlag extends FlagQueryParamMatcher("owning")
 
   object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala
index 92830d2d..4106642f 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala
@@ -18,8 +18,7 @@ import docspell.common.{Ident, Timestamp}
 import docspell.restapi.model._
 import docspell.restserver.Config
 import docspell.restserver.auth.ShareCookieData
-import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator}
-import docspell.store.records.RShare
+import docspell.restserver.http4s.{ClientRequestInfo, QueryParam => QP, ResponseGenerator}
 
 import emil.MailAddress
 import emil.javamail.syntax._
@@ -35,9 +34,10 @@ object ShareRoutes {
     import dsl._
 
     HttpRoutes.of {
-      case GET -> Root =>
+      case GET -> Root :? QP.Query(q) :? QP.OwningFlag(owning) =>
+        val login = if (owning) Some(user.account.user) else None
         for {
-          all <- backend.share.findAll(user.account.collective)
+          all <- backend.share.findAll(user.account.collective, login, q)
           now <- Timestamp.current[F]
           res <- Ok(ShareList(all.map(mkShareDetail(now))))
         } yield res
@@ -111,7 +111,7 @@ object ShareRoutes {
 
   def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
     OShare.NewShare(
-      user.account.collective,
+      user.account,
       data.name,
       data.query,
       data.enabled,
@@ -124,6 +124,12 @@ object ShareRoutes {
       case OShare.ChangeResult.Success(id) => IdResult(true, msg, id)
       case OShare.ChangeResult.PublishUntilInPast =>
         IdResult(false, "Until date must not be in the past", Ident.unsafe(""))
+      case OShare.ChangeResult.NotFound =>
+        IdResult(
+          false,
+          "Share not found or not owner. Only the owner can update a share.",
+          Ident.unsafe("")
+        )
     }
 
   def mkBasicResult(r: OShare.ChangeResult, msg: => String): BasicResult =
@@ -131,20 +137,26 @@ object ShareRoutes {
       case OShare.ChangeResult.Success(_) => BasicResult(true, msg)
       case OShare.ChangeResult.PublishUntilInPast =>
         BasicResult(false, "Until date must not be in the past")
+      case OShare.ChangeResult.NotFound =>
+        BasicResult(
+          false,
+          "Share not found or not owner. Only the owner can update a share."
+        )
     }
 
-  def mkShareDetail(now: Timestamp)(r: RShare): ShareDetail =
+  def mkShareDetail(now: Timestamp)(r: OShare.ShareData): ShareDetail =
     ShareDetail(
-      r.id,
-      r.query,
-      r.name,
-      r.enabled,
-      r.publishAt,
-      r.publishUntil,
-      now > r.publishUntil,
-      r.password.isDefined,
-      r.views,
-      r.lastAccess
+      r.share.id,
+      r.share.query,
+      IdName(r.user.uid, r.user.login.id),
+      r.share.name,
+      r.share.enabled,
+      r.share.publishAt,
+      r.share.publishUntil,
+      now > r.share.publishUntil,
+      r.share.password.isDefined,
+      r.share.views,
+      r.share.lastAccess
     )
 
   def convertIn(s: SimpleShareMail): Either[String, ShareMail] =
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala
index 96202f14..64e14a5e 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala
@@ -59,7 +59,7 @@ object ShareSearchRoutes {
                 cfg.maxNoteLength,
                 searchMode = SearchMode.Normal
               )
-              account = share.asAccount
+              account = share.account
               fixQuery = Query.Fix(account, Some(share.query.expr), None)
               _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}")
               resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery)
diff --git a/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql
index 7e252c14..9765afc1 100644
--- a/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql
+++ b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql
@@ -1,6 +1,6 @@
 CREATE TABLE "item_share" (
   "id" varchar(254) not null primary key,
-  "cid" varchar(254) not null,
+  "user_id" varchar(254) not null,
   "name" varchar(254),
   "query" varchar(2000) not null,
   "enabled" boolean not null,
@@ -9,5 +9,5 @@ CREATE TABLE "item_share" (
   "publish_until" timestamp not null,
   "views" int not null,
   "last_access" timestamp,
-  foreign key ("cid") references "collective"("cid") on delete cascade
+  foreign key ("user_id") references "user_"("uid") on delete cascade
 )
diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql
index fb74d283..714aabbb 100644
--- a/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql
+++ b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql
@@ -1,6 +1,6 @@
 CREATE TABLE `item_share` (
   `id` varchar(254) not null primary key,
-  `cid` varchar(254) not null,
+  `user_id` varchar(254) not null,
   `name` varchar(254),
   `query` varchar(2000) not null,
   `enabled` boolean not null,
@@ -9,5 +9,5 @@ CREATE TABLE `item_share` (
   `publish_until` timestamp not null,
   `views` int not null,
   `last_access` timestamp,
-  foreign key (`cid`) references `collective`(`cid`) on delete cascade
+  foreign key (`user_id`) references `user_`(`uid`) on delete cascade
 )
diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql
index 7e252c14..9765afc1 100644
--- a/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql
+++ b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql
@@ -1,6 +1,6 @@
 CREATE TABLE "item_share" (
   "id" varchar(254) not null primary key,
-  "cid" varchar(254) not null,
+  "user_id" varchar(254) not null,
   "name" varchar(254),
   "query" varchar(2000) not null,
   "enabled" boolean not null,
@@ -9,5 +9,5 @@ CREATE TABLE "item_share" (
   "publish_until" timestamp not null,
   "views" int not null,
   "last_access" timestamp,
-  foreign key ("cid") references "collective"("cid") on delete cascade
+  foreign key ("user_id") references "user_"("uid") on delete cascade
 )
diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala
index 7d6ae9bd..5ddfdb6b 100644
--- a/modules/store/src/main/scala/docspell/store/records/RShare.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala
@@ -18,7 +18,7 @@ import doobie.implicits._
 
 final case class RShare(
     id: Ident,
-    cid: Ident,
+    userId: Ident,
     name: Option[String],
     query: ItemQuery,
     enabled: Boolean,
@@ -35,7 +35,7 @@ object RShare {
     val tableName = "item_share";
 
     val id = Column[Ident]("id", this)
-    val cid = Column[Ident]("cid", this)
+    val userId = Column[Ident]("user_id", this)
     val name = Column[String]("name", this)
     val query = Column[ItemQuery]("query", this)
     val enabled = Column[Boolean]("enabled", this)
@@ -48,7 +48,7 @@ object RShare {
     val all: NonEmptyList[Column[_]] =
       NonEmptyList.of(
         id,
-        cid,
+        userId,
         name,
         query,
         enabled,
@@ -67,7 +67,7 @@ object RShare {
     DML.insert(
       T,
       T.all,
-      fr"${r.id},${r.cid},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}"
+      fr"${r.id},${r.userId},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}"
     )
 
   def incAccess(id: Ident): ConnectionIO[Int] =
@@ -83,7 +83,7 @@ object RShare {
   def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] =
     DML.update(
       T,
-      T.id === r.id && T.cid === r.cid,
+      T.id === r.id && T.userId === r.userId,
       DML.set(
         T.name.setTo(r.name),
         T.query.setTo(r.query),
@@ -94,26 +94,41 @@ object RShare {
             else Nil)
     )
 
-  def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, RShare] =
+  def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, (RShare, RUser)] = {
+    val s = RShare.as("s")
+    val u = RUser.as("u")
+
     OptionT(
-      Select(select(T.all), from(T), T.id === id && T.cid === cid).build
-        .query[RShare]
+      Select(
+        select(s.all, u.all),
+        from(s).innerJoin(u, u.uid === s.userId),
+        s.id === id && u.cid === cid
+      ).build
+        .query[(RShare, RUser)]
         .option
     )
+  }
 
   private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition =
     t.id === id && t.enabled === true && t.publishedUntil > current
 
-  def findActive(id: Ident, current: Timestamp): OptionT[ConnectionIO, RShare] =
+  def findActive(
+      id: Ident,
+      current: Timestamp
+  ): OptionT[ConnectionIO, (RShare, RUser)] = {
+    val s = RShare.as("s")
+    val u = RUser.as("u")
+
     OptionT(
       Select(
-        select(T.all),
-        from(T),
-        activeCondition(T, id, current)
-      ).build.query[RShare].option
+        select(s.all, u.all),
+        from(s).innerJoin(u, s.userId === u.uid),
+        activeCondition(s, id, current)
+      ).build.query[(RShare, RUser)].option
     )
+  }
 
-  def findCurrentActive(id: Ident): OptionT[ConnectionIO, RShare] =
+  def findCurrentActive(id: Ident): OptionT[ConnectionIO, (RShare, RUser)] =
     OptionT.liftF(Timestamp.current[ConnectionIO]).flatMap(now => findActive(id, now))
 
   def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] =
@@ -123,13 +138,30 @@ object RShare {
         .option
     })
 
-  def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] =
-    Select(select(T.all), from(T), T.cid === cid)
-      .orderBy(T.publishedAt.desc)
-      .build
-      .query[RShare]
-      .to[List]
+  def findAllByCollective(
+      cid: Ident,
+      ownerLogin: Option[Ident],
+      q: Option[String]
+  ): ConnectionIO[List[(RShare, RUser)]] = {
+    val s = RShare.as("s")
+    val u = RUser.as("u")
 
-  def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] =
-    DML.delete(T, T.id === id && T.cid === cid)
+    val ownerQ = ownerLogin.map(name => u.login === name)
+    val nameQ = q.map(n => s.name.like(s"%$n%"))
+
+    Select(
+      select(s.all, u.all),
+      from(s).innerJoin(u, u.uid === s.userId),
+      u.cid === cid &&? ownerQ &&? nameQ
+    )
+      .orderBy(s.publishedAt.desc)
+      .build
+      .query[(RShare, RUser)]
+      .to[List]
+  }
+
+  def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] = {
+    val u = RUser.T
+    DML.delete(T, T.id === id && T.userId.in(Select(u.uid.s, from(u), u.cid === cid)))
+  }
 }
diff --git a/modules/store/src/main/scala/docspell/store/records/RUser.scala b/modules/store/src/main/scala/docspell/store/records/RUser.scala
index dc8f66d8..dbd4051c 100644
--- a/modules/store/src/main/scala/docspell/store/records/RUser.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RUser.scala
@@ -26,7 +26,13 @@ case class RUser(
     loginCount: Int,
     lastLogin: Option[Timestamp],
     created: Timestamp
-) {}
+) {
+  def accountId: AccountId =
+    AccountId(cid, login)
+
+  def idRef: IdRef =
+    IdRef(uid, login.id)
+}
 
 object RUser {
 
diff --git a/modules/webapp/package-lock.json b/modules/webapp/package-lock.json
index 6f8016f3..4801d04f 100644
--- a/modules/webapp/package-lock.json
+++ b/modules/webapp/package-lock.json
@@ -153,20 +153,8 @@
             "electron-to-chromium": "^1.3.719",
             "escalade": "^3.1.1",
             "node-releases": "^1.1.71"
-          },
-          "dependencies": {
-            "caniuse-lite": {
-              "version": "1.0.30001230",
-              "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz",
-              "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ=="
-            }
           }
         },
-        "caniuse-lite": {
-          "version": "1.0.30001204",
-          "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz",
-          "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ=="
-        },
         "colorette": {
           "version": "1.2.2",
           "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
@@ -228,11 +216,6 @@
         "node-releases": "^1.1.71"
       },
       "dependencies": {
-        "caniuse-lite": {
-          "version": "1.0.30001230",
-          "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz",
-          "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ=="
-        },
         "colorette": {
           "version": "1.2.2",
           "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
@@ -272,9 +255,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001208",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz",
-      "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA=="
+      "version": "1.0.30001271",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz",
+      "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA=="
     },
     "chalk": {
       "version": "2.4.2",
diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm
index 2051fe23..bb89794d 100644
--- a/modules/webapp/src/main/elm/Api.elm
+++ b/modules/webapp/src/main/elm/Api.elm
@@ -2228,10 +2228,19 @@ disableOtp flags otp receive =
 --- Share
 
 
-getShares : Flags -> (Result Http.Error ShareList -> msg) -> Cmd msg
-getShares flags receive =
+getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg
+getShares flags query owning receive =
     Http2.authGet
-        { url = flags.config.baseUrl ++ "/api/v1/sec/share"
+        { url =
+            flags.config.baseUrl
+                ++ "/api/v1/sec/share?q="
+                ++ Url.percentEncode query
+                ++ (if owning then
+                        "&owning"
+
+                    else
+                        ""
+                   )
         , account = getAccount flags
         , expect = Http.expectJson receive Api.Model.ShareList.decoder
         }
diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm
index be079129..e1c33def 100644
--- a/modules/webapp/src/main/elm/Comp/ShareManage.elm
+++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm
@@ -56,6 +56,8 @@ type alias Model =
     , loading : Bool
     , formError : FormError
     , deleteConfirm : DeleteConfirm
+    , query : String
+    , owningOnly : Bool
     }
 
 
@@ -75,6 +77,8 @@ init flags =
       , loading = False
       , formError = FormErrorNone
       , deleteConfirm = DeleteConfirmOff
+      , query = ""
+      , owningOnly = True
       }
     , Cmd.batch
         [ Cmd.map FormMsg fc
@@ -90,6 +94,8 @@ type Msg
     | MailMsg Comp.ShareMail.Msg
     | InitNewShare
     | SetViewMode ViewMode
+    | SetQuery String
+    | ToggleOwningOnly
     | Submit
     | RequestDelete
     | CancelDelete
@@ -126,7 +132,7 @@ update texts flags msg model =
         SetViewMode vm ->
             ( { model | viewMode = vm, formError = FormErrorNone }
             , if vm == Table then
-                Api.getShares flags LoadSharesResp
+                Api.getShares flags model.query model.owningOnly LoadSharesResp
 
               else
                 Cmd.none
@@ -165,7 +171,10 @@ update texts flags msg model =
             )
 
         LoadShares ->
-            ( { model | loading = True }, Api.getShares flags LoadSharesResp, Sub.none )
+            ( { model | loading = True }
+            , Api.getShares flags model.query model.owningOnly LoadSharesResp
+            , Sub.none
+            )
 
         LoadSharesResp (Ok list) ->
             ( { model | loading = False, shares = list.items, formError = FormErrorNone }
@@ -231,6 +240,26 @@ update texts flags msg model =
             in
             ( { model | mailModel = mm }, Cmd.map MailMsg mc, Sub.none )
 
+        SetQuery q ->
+            let
+                nm =
+                    { model | query = q }
+            in
+            ( nm
+            , Api.getShares flags nm.query nm.owningOnly LoadSharesResp
+            , Sub.none
+            )
+
+        ToggleOwningOnly ->
+            let
+                nm =
+                    { model | owningOnly = not model.owningOnly }
+            in
+            ( nm
+            , Api.getShares flags nm.query nm.owningOnly LoadSharesResp
+            , Sub.none
+            )
+
 
 setShare : Texts -> ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg )
 setShare texts share flags model =
@@ -271,7 +300,19 @@ viewTable texts model =
     div [ class "flex flex-col" ]
         [ MB.view
             { start =
-                []
+                [ MB.TextInput
+                    { tagger = SetQuery
+                    , value = model.query
+                    , placeholder = texts.basics.searchPlaceholder
+                    , icon = Just "fa fa-search"
+                    }
+                , MB.Checkbox
+                    { tagger = \_ -> ToggleOwningOnly
+                    , label = texts.showOwningSharesOnly
+                    , value = model.owningOnly
+                    , id = "share-toggle-owner"
+                    }
+                ]
             , end =
                 [ MB.PrimaryButton
                     { tagger = InitNewShare
@@ -295,6 +336,11 @@ viewForm texts settings flags model =
     let
         newShare =
             model.formModel.share.id == ""
+
+        isOwner =
+            Maybe.map .user flags.account
+                |> Maybe.map ((==) model.formModel.share.owner.name)
+                |> Maybe.withDefault False
     in
     div []
         [ Html.form []
@@ -305,20 +351,34 @@ viewForm texts settings flags model =
 
               else
                 h1 [ class S.header2 ]
-                    [ text <| Maybe.withDefault texts.noName model.formModel.share.name
-                    , div [ class "opacity-50 text-sm" ]
-                        [ text "Id: "
-                        , text model.formModel.share.id
+                    [ div [ class "flex flex-row items-center" ]
+                        [ div
+                            [ class "flex text-sm opacity-75 label mr-3"
+                            , classList [ ( "hidden", isOwner ) ]
+                            ]
+                            [ i [ class "fa fa-user mr-2" ] []
+                            , text model.formModel.share.owner.name
+                            ]
+                        , text <| Maybe.withDefault texts.noName model.formModel.share.name
+                        ]
+                    , div [ class "flex flex-row items-center" ]
+                        [ div [ class "opacity-50 text-sm flex-grow" ]
+                            [ text "Id: "
+                            , text model.formModel.share.id
+                            ]
                         ]
                     ]
             , MB.view
                 { start =
-                    [ MB.PrimaryButton
-                        { tagger = Submit
-                        , title = "Submit this form"
-                        , icon = Just "fa fa-save"
-                        , label = texts.basics.submit
-                        }
+                    [ MB.CustomElement <|
+                        B.primaryButton
+                            { handler = onClick Submit
+                            , title = "Submit this form"
+                            , icon = "fa fa-save"
+                            , label = texts.basics.submit
+                            , disabled = not isOwner
+                            , attrs = [ href "#" ]
+                            }
                     , MB.SecondaryButton
                         { tagger = SetViewMode Table
                         , title = texts.basics.backToList
@@ -360,7 +420,15 @@ viewForm texts settings flags model =
                     FormErrorSubmit m ->
                         text m
                 ]
-            , Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel)
+            , div
+                [ classList [ ( "hidden", isOwner ) ]
+                , class S.infoMessage
+                ]
+                [ text texts.notOwnerInfo
+                ]
+            , div [ classList [ ( "hidden", not isOwner ) ] ]
+                [ Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel)
+                ]
             , B.loadingDimmer
                 { active = model.loading
                 , label = texts.basics.loading
diff --git a/modules/webapp/src/main/elm/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Comp/ShareTable.elm
index b62f39b5..1082567d 100644
--- a/modules/webapp/src/main/elm/Comp/ShareTable.elm
+++ b/modules/webapp/src/main/elm/Comp/ShareTable.elm
@@ -56,7 +56,10 @@ view texts shares =
                 , th [ class "text-center" ]
                     [ text texts.active
                     ]
-                , th [ class "text-center" ]
+                , th [ class "hidden sm:table-cell text-center" ]
+                    [ text texts.user
+                    ]
+                , th [ class "hidden sm:table-cell text-center" ]
                     [ text texts.publishUntil
                     ]
                 ]
@@ -88,6 +91,9 @@ renderShareLine texts share =
               else
                 i [ class "fa fa-check" ] []
             ]
+        , td [ class "hidden sm:table-cell text-center" ]
+            [ text share.owner.name
+            ]
         , td [ class "hidden sm:table-cell text-center" ]
             [ texts.formatDateTime share.publishUntil |> text
             ]
diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm
index d90824be..773ea5b3 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm
@@ -39,6 +39,8 @@ type alias Texts =
     , noName : String
     , shareInformation : String
     , sendMail : String
+    , notOwnerInfo : String
+    , showOwningSharesOnly : String
     }
 
 
@@ -62,6 +64,8 @@ gb =
     , noName = "No Name"
     , shareInformation = "Share Information"
     , sendMail = "Send via E-Mail"
+    , notOwnerInfo = "Only the user who created this share can edit its properties."
+    , showOwningSharesOnly = "Show my shares only"
     }
 
 
@@ -85,4 +89,6 @@ de =
     , noName = "Ohne Name"
     , shareInformation = "Informationen zur Freigabe"
     , sendMail = "Per E-Mail versenden"
+    , notOwnerInfo = "Nur der Benutzer, der diese Freigabe erstellt hat, kann diese auch ändern."
+    , showOwningSharesOnly = "Nur meine Freigaben anzeigen"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm
index 5b87e47e..170876ff 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm
@@ -21,6 +21,7 @@ type alias Texts =
     , formatDateTime : Int -> String
     , active : String
     , publishUntil : String
+    , user : String
     }
 
 
@@ -30,6 +31,7 @@ gb =
     , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English
     , active = "Active"
     , publishUntil = "Publish Until"
+    , user = "User"
     }
 
 
@@ -39,4 +41,5 @@ de =
     , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German
     , active = "Aktiv"
     , publishUntil = "Publiziert bis"
+    , user = "Benutzer"
     }