From dbaa606a9f71f4a4353be46d13f914196fef0c17 Mon Sep 17 00:00:00 2001
From: Michael Shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 5 Dec 2022 21:02:56 -0800
Subject: [PATCH] add django-guardian, djangorestframework-guardian

---
 Pipfile                                       |  2 +
 Pipfile.lock                                  | 43 ++++++---
 src/documents/admin.py                        |  2 +-
 ...dview_user_correspondent_owner_and_more.py | 87 +++++++++++++++++++
 src/documents/models.py                       | 26 ++++--
 src/documents/views.py                        |  4 +-
 src/paperless/settings.py                     |  2 +
 .../0017_mailaccount_owner_mailrule_owner.py  | 38 ++++++++
 src/paperless_mail/models.py                  |  4 +-
 9 files changed, 183 insertions(+), 25 deletions(-)
 create mode 100644 src/documents/migrations/1028_remove_savedview_user_correspondent_owner_and_more.py
 create mode 100644 src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py

diff --git a/Pipfile b/Pipfile
index e7702898a..dcbd6a611 100644
--- a/Pipfile
+++ b/Pipfile
@@ -63,6 +63,8 @@ flower = "*"
 bleach = "*"
 # https://www.piwheels.org/project/cryptography/ last built version
 cryptography = "==38.0.1"
+django-guardian = "*"
+djangorestframework-guardian = "*"
 
 [dev-packages]
 coveralls = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 844668913..c2bc7bcce 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "cbfe9920231de6e7f993962efb3cc371abdb6b08975232d4cf64d1bad1b53d7a"
+            "sha256": "f3eaa281038ee70cde11a1bf32573ed51d5e09baae13e2d9a1b082d751d07330"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -227,7 +227,7 @@
                 "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
                 "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
             ],
-            "markers": "python_full_version >= '3.6.0'",
+            "markers": "python_version >= '3.6'",
             "version": "==2.1.1"
         },
         "click": {
@@ -313,7 +313,6 @@
                 "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.6'",
             "version": "==38.0.1"
         },
         "daphne": {
@@ -387,6 +386,14 @@
             "index": "pypi",
             "version": "==22.1"
         },
+        "django-guardian": {
+            "hashes": [
+                "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697",
+                "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"
+            ],
+            "index": "pypi",
+            "version": "==2.4.0"
+        },
         "djangorestframework": {
             "hashes": [
                 "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",
@@ -395,6 +402,14 @@
             "index": "pypi",
             "version": "==3.14.0"
         },
+        "djangorestframework-guardian": {
+            "hashes": [
+                "sha256:1883756452d9bfcc2a51fb4e039a6837a8f6697c756447aa83af085749b59330",
+                "sha256:3bd3dd6ea58e1bceca5048faf6f8b1a93bb5dcff30ba5eb91b9a0e190a48a0c7"
+            ],
+            "index": "pypi",
+            "version": "==0.3.0"
+        },
         "filelock": {
             "hashes": [
                 "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc",
@@ -1397,7 +1412,7 @@
                 "sha256:f51dcb39e910a853749250c0f82aced80bca3f7315e9c4ee14349eb7cab6a3f8",
                 "sha256:f5808e1dac6b66c109d6205ce2aebf84bb89e1a1493b7e6df38932df5ebfb9cf"
             ],
-            "markers": "python_version >= '3.7' and python_version < '4'",
+            "markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
             "version": "==3.6.12"
         },
         "requests": {
@@ -1405,7 +1420,7 @@
                 "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
                 "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
             ],
-            "markers": "python_version >= '3.7' and python_version < '4'",
+            "markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
             "version": "==2.28.1"
         },
         "scikit-learn": {
@@ -1633,7 +1648,7 @@
                 "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
                 "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
             ],
-            "markers": "python_version >= '3.7'",
+            "markers": "python_version < '3.10'",
             "version": "==4.4.0"
         },
         "tzdata": {
@@ -1657,7 +1672,7 @@
                 "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
                 "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_full_version < '4.0.0'",
             "version": "==1.26.12"
         },
         "uvicorn": {
@@ -2070,7 +2085,7 @@
                 "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
                 "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
             ],
-            "markers": "python_full_version >= '3.6.0'",
+            "markers": "python_version >= '3.6'",
             "version": "==2.1.1"
         },
         "click": {
@@ -2090,7 +2105,9 @@
             "version": "==0.4.6"
         },
         "coverage": {
-            "extras": [],
+            "extras": [
+                "toml"
+            ],
             "hashes": [
                 "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79",
                 "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a",
@@ -2749,7 +2766,7 @@
                 "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
                 "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
             ],
-            "markers": "python_version >= '3.7' and python_version < '4'",
+            "markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
             "version": "==2.28.1"
         },
         "scipy": {
@@ -2896,7 +2913,7 @@
                 "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
                 "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
             ],
-            "markers": "python_full_version < '3.11.0a7'",
+            "markers": "python_version < '3.11'",
             "version": "==2.0.1"
         },
         "tornado": {
@@ -2929,7 +2946,7 @@
                 "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
                 "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
             ],
-            "markers": "python_version >= '3.7'",
+            "markers": "python_version < '3.10'",
             "version": "==4.4.0"
         },
         "urllib3": {
@@ -2937,7 +2954,7 @@
                 "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
                 "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_full_version < '4.0.0'",
             "version": "==1.26.12"
         },
         "virtualenv": {
diff --git a/src/documents/admin.py b/src/documents/admin.py
index 6fa06c49b..c51a5783c 100644
--- a/src/documents/admin.py
+++ b/src/documents/admin.py
@@ -97,7 +97,7 @@ class RuleInline(admin.TabularInline):
 
 class SavedViewAdmin(admin.ModelAdmin):
 
-    list_display = ("name", "user")
+    list_display = ("name", "owner")
 
     inlines = [RuleInline]
 
diff --git a/src/documents/migrations/1028_remove_savedview_user_correspondent_owner_and_more.py b/src/documents/migrations/1028_remove_savedview_user_correspondent_owner_and_more.py
new file mode 100644
index 000000000..d707028e3
--- /dev/null
+++ b/src/documents/migrations/1028_remove_savedview_user_correspondent_owner_and_more.py
@@ -0,0 +1,87 @@
+# Generated by Django 4.1.3 on 2022-12-06 04:48
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("documents", "1027_remove_paperlesstask_attempted_task_and_more"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="savedview",
+            old_name="user",
+            new_name="owner",
+        ),
+        migrations.AlterField(
+            model_name="savedview",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="correspondent",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="document",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="documenttype",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="storagepath",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="tag",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+    ]
diff --git a/src/documents/models.py b/src/documents/models.py
index c1b9c88bc..1e592a68d 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -57,14 +57,27 @@ class MatchingModel(models.Model):
         return self.name
 
 
-class Correspondent(MatchingModel):
+class ModelWithOwner(models.Model):
+    owner = models.ForeignKey(
+        User,
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        verbose_name=_("owner"),
+    )
+
+    class Meta:
+        abstract = True
+
+
+class Correspondent(MatchingModel, ModelWithOwner):
     class Meta:
         ordering = ("name",)
         verbose_name = _("correspondent")
         verbose_name_plural = _("correspondents")
 
 
-class Tag(MatchingModel):
+class Tag(MatchingModel, ModelWithOwner):
 
     color = models.CharField(_("color"), max_length=7, default="#a6cee3")
 
@@ -82,13 +95,13 @@ class Tag(MatchingModel):
         verbose_name_plural = _("tags")
 
 
-class DocumentType(MatchingModel):
+class DocumentType(MatchingModel, ModelWithOwner):
     class Meta:
         verbose_name = _("document type")
         verbose_name_plural = _("document types")
 
 
-class StoragePath(MatchingModel):
+class StoragePath(MatchingModel, ModelWithOwner):
     path = models.CharField(
         _("path"),
         max_length=512,
@@ -100,7 +113,7 @@ class StoragePath(MatchingModel):
         verbose_name_plural = _("storage paths")
 
 
-class Document(models.Model):
+class Document(ModelWithOwner):
 
     STORAGE_TYPE_UNENCRYPTED = "unencrypted"
     STORAGE_TYPE_GPG = "gpg"
@@ -356,14 +369,13 @@ class Log(models.Model):
         return self.message
 
 
-class SavedView(models.Model):
+class SavedView(ModelWithOwner):
     class Meta:
 
         ordering = ("name",)
         verbose_name = _("saved view")
         verbose_name_plural = _("saved views")
 
-    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("user"))
     name = models.CharField(_("name"), max_length=128)
 
     show_on_dashboard = models.BooleanField(
diff --git a/src/documents/views.py b/src/documents/views.py
index 9ffc23693..0657ae93a 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -556,10 +556,10 @@ class SavedViewViewSet(ModelViewSet):
 
     def get_queryset(self):
         user = self.request.user
-        return SavedView.objects.filter(user=user)
+        return SavedView.objects.filter(owner=user)
 
     def perform_create(self, serializer):
-        serializer.save(user=self.request.user)
+        serializer.save(owner=self.request.user)
 
 
 class BulkEditView(GenericAPIView):
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index c11e43489..dd078f0f9 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -171,6 +171,7 @@ INSTALLED_APPS = [
     "rest_framework.authtoken",
     "django_filters",
     "django_celery_results",
+    "guardian",
 ] + env_apps
 
 if DEBUG:
@@ -276,6 +277,7 @@ if ENABLE_HTTP_REMOTE_USER:
     AUTHENTICATION_BACKENDS = [
         "django.contrib.auth.backends.RemoteUserBackend",
         "django.contrib.auth.backends.ModelBackend",
+        "guardian.backends.ObjectPermissionBackend",
     ]
     REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
         "rest_framework.authentication.RemoteUserAuthentication",
diff --git a/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py b/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py
new file mode 100644
index 000000000..5eeccf440
--- /dev/null
+++ b/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.1.3 on 2022-12-06 04:48
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("paperless_mail", "0016_mailrule_consumption_scope"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="mailaccount",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+        migrations.AddField(
+            model_name="mailrule",
+            name="owner",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="owner",
+            ),
+        ),
+    ]
diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py
index a7267cc06..f8a30c3be 100644
--- a/src/paperless_mail/models.py
+++ b/src/paperless_mail/models.py
@@ -3,7 +3,7 @@ from django.db import models
 from django.utils.translation import gettext_lazy as _
 
 
-class MailAccount(models.Model):
+class MailAccount(document_models.ModelWithOwner):
     class Meta:
         verbose_name = _("mail account")
         verbose_name_plural = _("mail accounts")
@@ -51,7 +51,7 @@ class MailAccount(models.Model):
         return self.name
 
 
-class MailRule(models.Model):
+class MailRule(document_models.ModelWithOwner):
     class Meta:
         verbose_name = _("mail rule")
         verbose_name_plural = _("mail rules")