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