From ff0887b9d203c8916b2b6d2135be61d308955e13 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sat, 3 Jul 2021 11:29:13 +0200
Subject: [PATCH] Add permissions for everything

---
 aleksis/apps/resint/menus.py                  |  32 ++++--
 .../resint/migrations/0002_permissions.py     |  17 +++
 aleksis/apps/resint/models.py                 |   5 +
 aleksis/apps/resint/rules.py                  | 105 ++++++++++++++++++
 .../resint/templates/resint/group/list.html   |  38 ++++---
 .../resint/templates/resint/poster/list.html  |  25 +++--
 aleksis/apps/resint/views.py                  |  43 +++++--
 7 files changed, 227 insertions(+), 38 deletions(-)
 create mode 100644 aleksis/apps/resint/migrations/0002_permissions.py
 create mode 100644 aleksis/apps/resint/rules.py

diff --git a/aleksis/apps/resint/menus.py b/aleksis/apps/resint/menus.py
index 22c93b0..983f95d 100644
--- a/aleksis/apps/resint/menus.py
+++ b/aleksis/apps/resint/menus.py
@@ -17,9 +17,15 @@ def _get_menu_entries() -> List[Dict[str, Any]]:
             "name": group.name,
             "url": reverse("poster_show_current", args=[group.slug]),
             "icon": "picture_as_pdf",
-            "validators": ["menu_generator.validators.is_authenticated"],
+            "validators": [
+                (
+                    "aleksis.apps.resint.rules.permission_validator",
+                    "resint.view_poster_pdf_menu",
+                    group,
+                ),
+            ],
         }
-        for group in PosterGroup.objects.filter(show_in_menu=True)
+        for group in PosterGroup.objects.all()
     ]
 
 
@@ -32,22 +38,34 @@ MENUS = {
             "url": "#",
             "icon": "open_in_browser",
             "root": True,
-            "validators": ["menu_generator.validators.is_authenticated",],
+            "validators": [
+                ("aleksis.core.util.predicates.permission_validator", "resint.view_poster_menu",),
+            ],
             "submenu": [
                 {
                     "name": _("Manage posters"),
                     "url": "poster_index",
                     "icon": "file_upload",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "resint.view_posters_rule",
+                        ),
+                    ],
                 },
                 {
                     "name": _("Poster groups"),
                     "url": "poster_group_list",
                     "icon": "topic",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "resint.view_postergroups_rule",
+                        ),
+                    ],
                 },
-            ]
-            + get_menu_entries_lazy(),
+            ],
         }
     ]
+    + get_menu_entries_lazy(),
 }
diff --git a/aleksis/apps/resint/migrations/0002_permissions.py b/aleksis/apps/resint/migrations/0002_permissions.py
new file mode 100644
index 0000000..d1595a7
--- /dev/null
+++ b/aleksis/apps/resint/migrations/0002_permissions.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.5 on 2021-07-03 09:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('resint', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='postergroup',
+            options={'permissions': [('view_poster_of_group', 'Can view all posters of this group'), ('upload_poster_to_group', 'Can upload new posters to this group'), ('delete_poster_of_group', 'Can delete all posters of this group')], 'verbose_name': 'Poster group', 'verbose_name_plural': 'Poster groups'},
+        ),
+    ]
diff --git a/aleksis/apps/resint/models.py b/aleksis/apps/resint/models.py
index 2a84336..f5a7d3f 100644
--- a/aleksis/apps/resint/models.py
+++ b/aleksis/apps/resint/models.py
@@ -41,6 +41,11 @@ class PosterGroup(ExtensibleModel):
             models.UniqueConstraint(fields=["site_id", "name"], name="unique_site_name"),
             models.UniqueConstraint(fields=["site_id", "slug"], name="unique_site_slug"),
         ]
+        permissions = [
+            ("view_poster_of_group", _("Can view all posters of this group")),
+            ("upload_poster_to_group", _("Can upload new posters to this group")),
+            ("delete_poster_of_group", _("Can delete all posters of this group")),
+        ]
 
     def __str__(self) -> str:
         return f"{self.name} ({self.publishing_day_name}, {self.publishing_time})"
diff --git a/aleksis/apps/resint/rules.py b/aleksis/apps/resint/rules.py
new file mode 100644
index 0000000..53d2f7a
--- /dev/null
+++ b/aleksis/apps/resint/rules.py
@@ -0,0 +1,105 @@
+from django.contrib.auth.models import User
+from django.http import HttpRequest
+
+from rules import add_perm, predicate
+
+from aleksis.apps.resint.models import Poster, PosterGroup
+from aleksis.core.util.predicates import (
+    check_object_permission,
+    has_any_object,
+    has_global_perm,
+    has_object_perm,
+    has_person,
+)
+
+
+def has_poster_group_object_perm(perm: str):
+    name = f"has_poster_group_object_perm:{perm}"
+
+    @predicate(name)
+    def fn(user: User, obj: Poster) -> bool:
+        return check_object_permission(user, perm, obj.group, checker_obj=obj)
+
+    return fn
+
+
+def permission_validator(request: HttpRequest, perm: str, obj) -> bool:
+    """Check whether the request user has a permission."""
+    print("Am I executed?")
+    if request.user:
+        print("And I", request, request.user, perm, obj)
+        print(request.user.has_perm(perm, obj))
+        return request.user.has_perm(perm, obj)
+    return False
+
+
+@predicate
+def is_public_poster_group(user: User, obj: PosterGroup):
+    return obj.public
+
+
+@predicate
+def show_poster_group_in_menu(user: User, obj: PosterGroup):
+    return obj.show_in_menu
+
+
+# View poster group list
+view_poster_groups_predicate = has_person & (
+    has_global_perm("resint.view_postergroup")
+    | has_any_object("resint.view_postergroup", PosterGroup)
+)
+add_perm("resint.view_postergroups_rule", view_poster_groups_predicate)
+
+# Add poster group
+add_poster_group_predicate = view_poster_groups_predicate & has_global_perm(
+    "resint.add_postergroup"
+)
+add_perm("resint.add_postergroup_rule", add_poster_group_predicate)
+
+# Edit poster group
+edit_poster_group_predicate = view_poster_groups_predicate & (
+    has_global_perm("resint.change_postergroup") | has_object_perm("resint.change_postergroup")
+)
+add_perm("resint.edit_postergroup_rule", edit_poster_group_predicate)
+
+# Delete poster group
+delete_poster_group_predicate = view_poster_groups_predicate & (
+    has_global_perm("resint.delete_postergroup") | has_object_perm("resint.delete_postergroup")
+)
+add_perm("resint.delete_postergroup_rule", delete_poster_group_predicate)
+
+view_posters_predicate = has_person & (
+    has_global_perm("resint.view_poster")
+    | has_any_object("resint.view_poster", Poster)
+    | has_any_object("resint.view_poster_of_group", PosterGroup)
+)
+add_perm("resint.view_posters_rule", view_posters_predicate)
+
+# Upload poster
+upload_poster_predicate = view_posters_predicate & (
+    has_global_perm("resint.add_poster") | has_any_object("resint.add_poster_to_group", PosterGroup)
+)  # FIXME FIlter on form
+add_perm("resint.upload_poster_rule", upload_poster_predicate)
+
+# Delete poster
+delete_poster_predicate = view_posters_predicate & (
+    has_global_perm("resint.delete_poster")
+    | has_object_perm("resint.delete_poster")
+    | has_poster_group_object_perm("resint.delete_poster_of_group")
+)
+add_perm("resint.delete_poster_rule", delete_poster_predicate)
+
+# View poster PDF file
+view_poster_pdf_predicate = is_public_poster_group | (
+    has_person
+    & (has_global_perm("resint.view_postergroup") | has_global_perm("resint.view_poster"))
+)
+add_perm("resint.view_poster_pdf", view_poster_pdf_predicate)
+
+# View menu entry for single posters
+view_poster_pdf_menu_predicate = show_poster_group_in_menu & view_poster_pdf_predicate
+add_perm("resint.view_poster_pdf_menu", view_poster_pdf_menu_predicate)
+
+# Show the poster manage menu
+view_poster_menu_predicate = view_posters_predicate | view_poster_groups_predicate
+add_perm("resint.view_poster_menu", view_poster_menu_predicate)
diff --git a/aleksis/apps/resint/templates/resint/group/list.html b/aleksis/apps/resint/templates/resint/group/list.html
index 00b2d3c..a0e248f 100644
--- a/aleksis/apps/resint/templates/resint/group/list.html
+++ b/aleksis/apps/resint/templates/resint/group/list.html
@@ -1,12 +1,15 @@
 {% extends 'core/base.html' %}
-{% load material_form i18n %}
+{% load material_form i18n rules %}
 
 {% block browser_title %}{% blocktrans %}Poster groups{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  <a class="waves-effect waves-light btn green modal-trigger right" href="{% url "create_poster_group" %}">
-    <i class="material-icons left">add</i>{% blocktrans %}Create new poster group{% endblocktrans %}
-  </a>
+  {% has_perm "resint.add_postergroup" user as can_add_poster_group %}
+  {% if can_add_poster_group %}
+    <a class="waves-effect waves-light btn green modal-trigger right" href="{% url "create_poster_group" %}">
+      <i class="material-icons left">add</i>{% blocktrans %}Create new poster group{% endblocktrans %}
+    </a>
+  {% endif %}
 
   <h1>{% blocktrans %}Poster groups{% endblocktrans %}</h1>
 
@@ -37,16 +40,23 @@
           </a>
         </td>
         <td>
-          <a href="{% url 'edit_poster_group' poster_group.id %}"
-             class="waves-effect waves-light btn-flat orange-text">
-            <i class="material-icons left">edit</i>
-            {% trans "Edit" %}
-          </a>
-          <a href="{% url 'delete_poster_group' poster_group.id %}"
-             class="waves-effect waves-light btn-flat red-text">
-            <i class="material-icons left">delete</i>
-            {% trans "Delete" %}
-          </a>
+          {% has_perm "resint.edit_postergroup" user poster_group as can_edit_poster_group %}
+          {% if can_edit_poster_group %}
+            <a href="{% url 'edit_poster_group' poster_group.id %}"
+               class="waves-effect waves-light btn-flat orange-text">
+              <i class="material-icons left">edit</i>
+              {% trans "Edit" %}
+            </a>
+          {% endif %}
+
+          {% has_perm "resint.delete_postergroup" user poster_group as can_delete_poster_group %}
+          {% if can_delete_poster_group %}
+            <a href="{% url 'delete_poster_group' poster_group.id %}"
+               class="waves-effect waves-light btn-flat red-text">
+              <i class="material-icons left">delete</i>
+              {% trans "Delete" %}
+            </a>
+          {% endif %}
         </td>
       </tr>
     {% empty %}
diff --git a/aleksis/apps/resint/templates/resint/poster/list.html b/aleksis/apps/resint/templates/resint/poster/list.html
index 3970ffe..f152f85 100644
--- a/aleksis/apps/resint/templates/resint/poster/list.html
+++ b/aleksis/apps/resint/templates/resint/poster/list.html
@@ -1,5 +1,5 @@
 {% extends "core/base.html" %}
-{% load static i18n %}
+{% load static i18n rules %}
 
 {% block content %}
 
@@ -42,10 +42,15 @@
       </div>
     {% endfor %}
   </div>
-  <a class="waves-effect waves-light btn green right" href="{% url "poster_upload" %}">
-    <i class="material-icons left">add</i>
-    {% trans "Upload new poster" %}
-  </a>
+
+  {% has_perm "resint.upload_poster" user as can_upload_poster %}
+  {% if can_upload_poster %}
+    <a class="waves-effect waves-light btn green right" href="{% url "poster_upload" %}">
+      <i class="material-icons left">add</i>
+      {% trans "Upload new poster" %}
+    </a>
+  {% endif %}
+
   <h2>{% trans "All uploaded posters" %}</h2>
   <table>
     <thead>
@@ -65,9 +70,13 @@
           <a class="btn-flat waves-effect waves-green" href="{{ poster.pdf.url }}" target="_blank">
             <i class="material-icons left">picture_as_pdf</i> {% trans "Show" %}
           </a>
-          <a class="btn-flat red-text waves-effect waves-red" href="{% url "poster_delete"  poster.id %}">
-            <i class="material-icons left">delete</i> {% trans "Delete" %}
-          </a>
+
+          {% has_perm "resint.delete_poster" user poster as can_delete_poster %}
+          {% if can_delete_poster %}
+            <a class="btn-flat red-text waves-effect waves-red" href="{% url "poster_delete"  poster.id %}">
+              <i class="material-icons left">delete</i> {% trans "Delete" %}
+            </a>
+          {% endif %}
         </td>
       </tr>
     {% endfor %}
diff --git a/aleksis/apps/resint/views.py b/aleksis/apps/resint/views.py
index 4b1be59..bfd6cee 100644
--- a/aleksis/apps/resint/views.py
+++ b/aleksis/apps/resint/views.py
@@ -8,20 +8,30 @@ from django.views import View
 from django.views.generic.detail import SingleObjectMixin
 from django.views.generic.list import ListView
 
+from guardian.shortcuts import get_objects_for_user
+from rules.contrib.views import PermissionRequiredMixin
+
 from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
 
 from .forms import PosterGroupForm, PosterUploadForm
 from .models import Poster, PosterGroup
 
 
-class PosterGroupListView(ListView):
+class PosterGroupListView(PermissionRequiredMixin, ListView):
     """Show a list of all poster groups."""
 
     template_name = "resint/group/list.html"
     model = PosterGroup
+    permission_required = "resint.view_postergroups_rule"
+
+    def get_queryset(self) -> QuerySet:
+        qs = super().get_queryset()
+        if self.request.user.has_perm("resint.view_postergroup"):
+            return qs
+        return get_objects_for_user(self.request.user, "resint.view_postergroup", qs)
 
 
-class PosterGroupCreateView(AdvancedCreateView):
+class PosterGroupCreateView(PermissionRequiredMixin, AdvancedCreateView):
     """Create a new poster group."""
 
     model = PosterGroup
@@ -29,9 +39,10 @@ class PosterGroupCreateView(AdvancedCreateView):
     template_name = "resint/group/create.html"
     success_message = _("The poster group has been saved.")
     form_class = PosterGroupForm
+    permission_required = "resint.create_postergroup_rule"
 
 
-class PosterGroupEditView(AdvancedEditView):
+class PosterGroupEditView(PermissionRequiredMixin, AdvancedEditView):
     """Edit an existing poster group."""
 
     model = PosterGroup
@@ -39,25 +50,36 @@ class PosterGroupEditView(AdvancedEditView):
     template_name = "resint/group/edit.html"
     success_message = _("The poster group has been saved.")
     form_class = PosterGroupForm
+    permission_required = "resint.edit_postergroup_rule"
 
 
-class PosterGroupDeleteView(AdvancedDeleteView):
+class PosterGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """Delete a poster group."""
 
     model = PosterGroup
     success_url = reverse_lazy("poster_group_list")
     success_message = _("The poster group has been deleted.")
     template_name = "core/pages/delete.html"
+    permission_required = "resint.delete_postergroup_rule"
 
 
-class PosterListView(ListView):
+class PosterListView(PermissionRequiredMixin, ListView):
     """Show a list of all uploaded posters."""
 
     template_name = "resint/poster/list.html"
     model = Poster
+    permission_required = "resint.view_posters_rule"
 
     def get_queryset(self) -> QuerySet:
-        return Poster.objects.all().order_by("-year", "-week")
+        qs = Poster.objects.all().order_by("-year", "-week")
+
+        if self.request.user.has_perm("resint.view_poster"):
+            return qs
+
+        allowed_groups = get_objects_for_user(self.request.user, "resint.view_poster_of_group", PosterGroup)
+        posters = get_objects_for_user(self.request.user, "resint.view_poster", qs)
+        return qs.filter(group__in=allowed_groups) | posters
+
 
     def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
         context = super().get_context_data(**kwargs)
@@ -65,7 +87,7 @@ class PosterListView(ListView):
         return context
 
 
-class PosterUploadView(AdvancedCreateView):
+class PosterUploadView(PermissionRequiredMixin, AdvancedCreateView):
     """Upload a new poster."""
 
     model = Poster
@@ -73,21 +95,24 @@ class PosterUploadView(AdvancedCreateView):
     template_name = "resint/poster/upload.html"
     success_message = _("The poster has been uploaded.")
     form_class = PosterUploadForm
+    permission_required = "resint.upload_poster_rule"
 
 
-class PosterDeleteView(AdvancedDeleteView):
+class PosterDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """Delete an uploaded poster."""
 
     model = Poster
     success_url = reverse_lazy("poster_index")
     success_message = _("The poster has been deleted.")
     template_name = "core/pages/delete.html"
+    permission_required = "resint.delete_poster_rule"
 
 
-class PosterCurrentView(SingleObjectMixin, View):
+class PosterCurrentView(PermissionRequiredMixin, SingleObjectMixin, View):
     """Show the poster which is currently valid."""
 
     model = PosterGroup
+    permission_required = "resint.view_poster_pdf"
 
     def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> FileResponse:
         group = self.get_object()
-- 
GitLab