From ad467fae2856672c6d5b365d3663e9894687add7 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sun, 26 Apr 2020 17:21:44 +0200 Subject: [PATCH] added basic group permission system --- cookbook/helper/group_helper.py | 53 +++++++++++++++++++ .../migrations/0034_auto_20200426_1614.py | 22 ++++++++ cookbook/tests/test_setup.py | 5 +- cookbook/views/api.py | 7 +-- cookbook/views/data.py | 11 ++-- cookbook/views/delete.py | 29 ++++++---- cookbook/views/edit.py | 33 +++++++----- cookbook/views/import_export.py | 3 ++ cookbook/views/lists.py | 14 ++--- cookbook/views/new.py | 20 ++++--- cookbook/views/views.py | 13 +++-- 11 files changed, 157 insertions(+), 53 deletions(-) create mode 100644 cookbook/helper/group_helper.py create mode 100644 cookbook/migrations/0034_auto_20200426_1614.py diff --git a/cookbook/helper/group_helper.py b/cookbook/helper/group_helper.py new file mode 100644 index 00000000..a7474e64 --- /dev/null +++ b/cookbook/helper/group_helper.py @@ -0,0 +1,53 @@ +""" +Source: https://djangosnippets.org/snippets/1703/ +""" +from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test +from django.utils.translation import gettext as _ +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy + + +def get_allowed_groups(groups_required): + groups_allowed = tuple(groups_required) + if 'guest' in groups_required: + groups_allowed = groups_allowed + ('user', 'admin') + if 'user' in groups_required: + groups_allowed = groups_allowed + ('admin',) + return groups_allowed + + +def group_required(*groups_required): + """Requires user membership in at least one of the groups passed in.""" + + def in_groups(u): + groups_allowed = get_allowed_groups(groups_required) + if u.is_authenticated: + if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)): + return True + return False + + return user_passes_test(in_groups, login_url='index') + + +class GroupRequiredMixin(object): + """ + groups_required - list of strings, required param + """ + + groups_required = None + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!')) + return HttpResponseRedirect(reverse_lazy('login')) + else: + if not request.user.is_superuser: + group_allowed = get_allowed_groups(self.groups_required) + user_groups = [] + for group in request.user.groups.values_list('name', flat=True): + user_groups.append(group) + if len(set(user_groups).intersection(group_allowed)) <= 0: + messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + return HttpResponseRedirect(reverse_lazy('index')) + return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs) diff --git a/cookbook/migrations/0034_auto_20200426_1614.py b/cookbook/migrations/0034_auto_20200426_1614.py new file mode 100644 index 00000000..ee3797aa --- /dev/null +++ b/cookbook/migrations/0034_auto_20200426_1614.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.5 on 2020-04-26 14:14 + +from django.db import migrations + + +def apply_migration(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Group.objects.bulk_create([ + Group(name=u'guest'), + Group(name=u'user'), + Group(name=u'admin'), + ]) + + +class Migration(migrations.Migration): + dependencies = [ + ('cookbook', '0033_userpreference_default_page'), + ] + + operations = [ + migrations.RunPython(apply_migration) + ] diff --git a/cookbook/tests/test_setup.py b/cookbook/tests/test_setup.py index 290c5ae1..f09ca911 100644 --- a/cookbook/tests/test_setup.py +++ b/cookbook/tests/test_setup.py @@ -1,5 +1,5 @@ from django.contrib import auth -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.test import TestCase, Client @@ -11,14 +11,17 @@ class TestBase(TestCase): self.client = Client() self.client.force_login(User.objects.get_or_create(username='client')[0]) user = auth.get_user(self.client) + user.groups.add(Group.objects.get(name='admin')) self.assertTrue(user.is_authenticated) self.another_client = Client() self.another_client.force_login(User.objects.get_or_create(username='another_client')[0]) user = auth.get_user(self.another_client) + user.groups.add(Group.objects.get(name='admin')) self.assertTrue(user.is_authenticated) self.superuser_client = Client() self.superuser_client.force_login(User.objects.get_or_create(username='superuser_client', is_superuser=True)[0]) user = auth.get_user(self.superuser_client) + user.groups.add(Group.objects.get(name='admin')) self.assertTrue(user.is_authenticated) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 5b878437..8206d4c6 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext as _ from django.contrib.auth.decorators import login_required from django.shortcuts import redirect +from cookbook.helper.group_helper import group_required from cookbook.models import Recipe, Sync, Storage from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud @@ -26,7 +27,7 @@ def update_recipe_links(recipe): recipe.save() -@login_required +@group_required('user') def get_external_file_link(request, recipe_id): recipe = Recipe.objects.get(id=recipe_id) if not recipe.link: @@ -35,7 +36,7 @@ def get_external_file_link(request, recipe_id): return HttpResponse(recipe.link) -@login_required +@group_required('user') def get_recipe_file(request, recipe_id): recipe = Recipe.objects.get(id=recipe_id) if not recipe.cors_link: @@ -44,7 +45,7 @@ def get_recipe_file(request, recipe_id): return HttpResponse(get_recipe_provider(recipe).get_base64_file(recipe)) -@login_required +@group_required('user') def sync_all(request): monitors = Sync.objects.filter(active=True) diff --git a/cookbook/views/data.py b/cookbook/views/data.py index d75bf36b..cfe6c70f 100644 --- a/cookbook/views/data.py +++ b/cookbook/views/data.py @@ -7,11 +7,12 @@ from django.utils.translation import ngettext from django_tables2 import RequestConfig from cookbook.forms import SyncForm, BatchEditForm +from cookbook.helper.group_helper import group_required from cookbook.models import * from cookbook.tables import SyncTable -@login_required +@group_required('user') def sync(request): if request.method == "POST": form = SyncForm(request.POST) @@ -31,12 +32,12 @@ def sync(request): return render(request, 'batch/monitor.html', {'form': form, 'monitored_paths': monitored_paths}) -@login_required +@group_required('user') def sync_wait(request): return render(request, 'batch/waiting.html') -@login_required +@group_required('user') def batch_import(request): imports = RecipeImport.objects.all() for new_recipe in imports: @@ -47,7 +48,7 @@ def batch_import(request): return redirect('list_recipe_import') -@login_required +@group_required('user') def batch_edit(request): if request.method == "POST": form = BatchEditForm(request.POST) @@ -86,7 +87,7 @@ class Object(object): pass -@login_required +@group_required('user') def statistics(request): counts = Object() counts.recipes = Recipe.objects.count() diff --git a/cookbook/views/delete.py b/cookbook/views/delete.py index 1bb0695d..c2d00d70 100644 --- a/cookbook/views/delete.py +++ b/cookbook/views/delete.py @@ -5,13 +5,15 @@ from django.urls import reverse_lazy, reverse from django.utils.translation import gettext as _ from django.views.generic import DeleteView +from cookbook.helper.group_helper import GroupRequiredMixin from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeBook, \ RecipeBookEntry, MealPlan, Ingredient from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud -class RecipeDelete(LoginRequiredMixin, DeleteView): +class RecipeDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = Recipe success_url = reverse_lazy('index') @@ -23,6 +25,7 @@ class RecipeDelete(LoginRequiredMixin, DeleteView): def delete_recipe_source(request, pk): + group_required = ['user'] recipe = get_object_or_404(Recipe, pk=pk) if recipe.storage.method == Storage.DROPBOX: @@ -38,7 +41,8 @@ def delete_recipe_source(request, pk): return HttpResponseRedirect(reverse('edit_recipe', args=[recipe.pk])) -class RecipeImportDelete(LoginRequiredMixin, DeleteView): +class RecipeImportDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = RecipeImport success_url = reverse_lazy('list_recipe_import') @@ -49,7 +53,8 @@ class RecipeImportDelete(LoginRequiredMixin, DeleteView): return context -class SyncDelete(LoginRequiredMixin, DeleteView): +class SyncDelete(GroupRequiredMixin, DeleteView): + groups_required = ['admin'] template_name = "generic/delete_template.html" model = Sync success_url = reverse_lazy('data_sync') @@ -60,7 +65,8 @@ class SyncDelete(LoginRequiredMixin, DeleteView): return context -class KeywordDelete(LoginRequiredMixin, DeleteView): +class KeywordDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = Keyword success_url = reverse_lazy('list_keyword') @@ -71,7 +77,8 @@ class KeywordDelete(LoginRequiredMixin, DeleteView): return context -class IngredientDelete(LoginRequiredMixin, DeleteView): +class IngredientDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = Ingredient success_url = reverse_lazy('list_ingredient') @@ -82,7 +89,8 @@ class IngredientDelete(LoginRequiredMixin, DeleteView): return context -class StorageDelete(LoginRequiredMixin, DeleteView): +class StorageDelete(GroupRequiredMixin, DeleteView): + groups_required = ['admin'] template_name = "generic/delete_template.html" model = Storage success_url = reverse_lazy('list_storage') @@ -104,7 +112,8 @@ class CommentDelete(LoginRequiredMixin, DeleteView): return context -class RecipeBookDelete(LoginRequiredMixin, DeleteView): +class RecipeBookDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = RecipeBook success_url = reverse_lazy('view_books') @@ -115,7 +124,8 @@ class RecipeBookDelete(LoginRequiredMixin, DeleteView): return context -class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView): +class RecipeBookEntryDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = RecipeBookEntry success_url = reverse_lazy('view_books') @@ -126,7 +136,8 @@ class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView): return context -class MealPlanDelete(LoginRequiredMixin, DeleteView): +class MealPlanDelete(GroupRequiredMixin, DeleteView): + groups_required = ['user'] template_name = "generic/delete_template.html" model = MealPlan success_url = reverse_lazy('view_plan') diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index d9b72f52..01893732 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -5,7 +5,6 @@ import simplejson import simplejson as json from PIL import Image from django.contrib import messages -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.core.files import File from django.http import HttpResponseRedirect @@ -16,13 +15,14 @@ from django.views.generic import UpdateView from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm, \ MealPlanForm, UnitMergeForm, IngredientMergeForm, IngredientForm +from cookbook.helper.group_helper import group_required, GroupRequiredMixin from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredient, RecipeBook, \ MealPlan, Unit, Ingredient from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud -@login_required +@group_required('guest') def switch_recipe(request, pk): recipe = get_object_or_404(Recipe, pk=pk) if recipe.internal: @@ -31,7 +31,7 @@ def switch_recipe(request, pk): return HttpResponseRedirect(reverse('edit_external_recipe', args=[pk])) -@login_required +@group_required('user') def convert_recipe(request, pk): recipe = get_object_or_404(Recipe, pk=pk) if not recipe.internal: @@ -41,7 +41,7 @@ def convert_recipe(request, pk): return HttpResponseRedirect(reverse('edit_internal_recipe', args=[pk])) -@login_required +@group_required('user') def internal_recipe_update(request, pk): recipe_instance = get_object_or_404(Recipe, pk=pk) status = 200 @@ -131,7 +131,8 @@ def internal_recipe_update(request, pk): 'view_url': reverse('view_recipe', args=[pk])}, status=status) -class SyncUpdate(LoginRequiredMixin, UpdateView): +class SyncUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['admin'] template_name = "generic/edit_template.html" model = Sync form_class = SyncForm @@ -147,7 +148,8 @@ class SyncUpdate(LoginRequiredMixin, UpdateView): return context -class KeywordUpdate(LoginRequiredMixin, UpdateView): +class KeywordUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] template_name = "generic/edit_template.html" model = Keyword form_class = KeywordForm @@ -163,7 +165,8 @@ class KeywordUpdate(LoginRequiredMixin, UpdateView): return context -class IngredientUpdate(LoginRequiredMixin, UpdateView): +class IngredientUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] template_name = "generic/edit_template.html" model = Ingredient form_class = IngredientForm @@ -179,7 +182,7 @@ class IngredientUpdate(LoginRequiredMixin, UpdateView): return context -@login_required +@group_required('admin') def edit_storage(request, pk): instance = get_object_or_404(Storage, pk=pk) @@ -239,7 +242,8 @@ class CommentUpdate(LoginRequiredMixin, UpdateView): return context -class ImportUpdate(LoginRequiredMixin, UpdateView): +class ImportUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] template_name = "generic/edit_template.html" model = RecipeImport fields = ['name', 'path'] @@ -255,7 +259,8 @@ class ImportUpdate(LoginRequiredMixin, UpdateView): return context -class RecipeBookUpdate(LoginRequiredMixin, UpdateView): +class RecipeBookUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] template_name = "generic/edit_template.html" model = RecipeBook fields = ['name'] @@ -271,7 +276,8 @@ class RecipeBookUpdate(LoginRequiredMixin, UpdateView): return context -class MealPlanUpdate(LoginRequiredMixin, UpdateView): +class MealPlanUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] template_name = "generic/edit_template.html" model = MealPlan form_class = MealPlanForm @@ -287,7 +293,8 @@ class MealPlanUpdate(LoginRequiredMixin, UpdateView): return context -class ExternalRecipeUpdate(LoginRequiredMixin, UpdateView): +class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView): + groups_required = ['user'] model = Recipe form_class = ExternalRecipeForm template_name = "generic/edit_template.html" @@ -322,7 +329,7 @@ class ExternalRecipeUpdate(LoginRequiredMixin, UpdateView): return context -@login_required +@group_required('user') def edit_ingredients(request): if request.method == "POST": success = False diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index b2a7a7a2..fdebbda3 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -11,9 +11,11 @@ from django.urls import reverse_lazy from django.utils.translation import gettext as _ from cookbook.forms import ExportForm, ImportForm +from cookbook.helper.group_helper import group_required from cookbook.models import RecipeIngredient, Recipe, Unit, Ingredient, Keyword +@group_required('user') def import_recipe(request): if request.method == "POST": form = ImportForm(request.POST) @@ -62,6 +64,7 @@ def import_recipe(request): return render(request, 'import.html', {'form': form}) +@group_required('user') def export_recipe(request): context = {} if request.method == "POST": diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index 348860cf..e33b2321 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -1,16 +1,16 @@ from django.contrib.auth.decorators import login_required from django.db.models.functions import Lower from django.shortcuts import render -from django.urls import reverse_lazy -from django_tables2 import RequestConfig from django.utils.translation import gettext as _ +from django_tables2 import RequestConfig from cookbook.filters import IngredientFilter +from cookbook.helper.group_helper import group_required from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Ingredient from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable -@login_required +@group_required('user') def keyword(request): table = KeywordTable(Keyword.objects.all()) RequestConfig(request, paginate={'per_page': 25}).configure(table) @@ -18,7 +18,7 @@ def keyword(request): return render(request, 'generic/list_template.html', {'title': _("Keyword"), 'table': table, 'create_url': 'new_keyword'}) -@login_required +@group_required('admin') def sync_log(request): table = ImportLogTable(SyncLog.objects.all().order_by(Lower('created_at').desc())) RequestConfig(request, paginate={'per_page': 25}).configure(table) @@ -26,7 +26,7 @@ def sync_log(request): return render(request, 'generic/list_template.html', {'title': _("Import Log"), 'table': table}) -@login_required +@group_required('user') def recipe_import(request): table = RecipeImportTable(RecipeImport.objects.all()) @@ -35,7 +35,7 @@ def recipe_import(request): return render(request, 'generic/list_template.html', {'title': _("Discovery"), 'table': table, 'import_btn': True}) -@login_required +@group_required('user') def ingredient(request): f = IngredientFilter(request.GET, queryset=Ingredient.objects.all().order_by('pk')) @@ -45,7 +45,7 @@ def ingredient(request): return render(request, 'generic/list_template.html', {'title': _("Ingredients"), 'table': table, 'filter': f}) -@login_required +@group_required('admin') def storage(request): table = StorageTable(Storage.objects.all()) RequestConfig(request, paginate={'per_page': 25}).configure(table) diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 198aefc8..f68d9c9e 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -2,8 +2,6 @@ import re from datetime import datetime from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.shortcuts import render, redirect from django.urls import reverse_lazy, reverse @@ -12,10 +10,12 @@ from django.views.generic import CreateView from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \ RecipeBookForm, MealPlanForm +from cookbook.helper.group_helper import GroupRequiredMixin, group_required from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan -class RecipeCreate(LoginRequiredMixin, CreateView): +class RecipeCreate(GroupRequiredMixin, CreateView): + groups_required = ['user'] template_name = "generic/new_template.html" model = Recipe fields = ('name',) @@ -36,7 +36,8 @@ class RecipeCreate(LoginRequiredMixin, CreateView): return context -class KeywordCreate(LoginRequiredMixin, CreateView): +class KeywordCreate(GroupRequiredMixin, CreateView): + groups_required = ['user'] template_name = "generic/new_template.html" model = Keyword form_class = KeywordForm @@ -48,7 +49,8 @@ class KeywordCreate(LoginRequiredMixin, CreateView): return context -class StorageCreate(LoginRequiredMixin, CreateView): +class StorageCreate(GroupRequiredMixin, CreateView): + groups_required = ['admin'] template_name = "generic/new_template.html" model = Storage form_class = StorageForm @@ -66,7 +68,7 @@ class StorageCreate(LoginRequiredMixin, CreateView): return context -@login_required +@group_required('user') def create_new_external_recipe(request, import_id): if request.method == "POST": form = ImportRecipeForm(request.POST) @@ -97,7 +99,8 @@ def create_new_external_recipe(request, import_id): return render(request, 'forms/edit_import_recipe.html', {'form': form}) -class RecipeBookCreate(LoginRequiredMixin, CreateView): +class RecipeBookCreate(GroupRequiredMixin, CreateView): + groups_required = ['user'] template_name = "generic/new_template.html" model = RecipeBook form_class = RecipeBookForm @@ -115,7 +118,8 @@ class RecipeBookCreate(LoginRequiredMixin, CreateView): return context -class MealPlanCreate(LoginRequiredMixin, CreateView): +class MealPlanCreate(GroupRequiredMixin, CreateView): + groups_required = ['user'] template_name = "generic/new_template.html" model = MealPlan form_class = MealPlanForm diff --git a/cookbook/views/views.py b/cookbook/views/views.py index c79b54eb..f76703b4 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -1,10 +1,8 @@ import copy -import re from datetime import datetime, timedelta from django.contrib import messages from django.contrib.auth import update_session_auth_hash -from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordChangeForm from django.http import HttpResponseRedirect from django.shortcuts import render, get_object_or_404 @@ -14,6 +12,7 @@ from django.utils.translation import gettext as _ from cookbook.filters import RecipeFilter from cookbook.forms import * +from cookbook.helper.group_helper import group_required from cookbook.tables import RecipeTable @@ -44,7 +43,7 @@ def search(request): return render(request, 'index.html') -@login_required +@group_required('guest') def recipe_view(request, pk): recipe = get_object_or_404(Recipe, pk=pk) ingredients = RecipeIngredient.objects.filter(recipe=recipe) @@ -80,7 +79,7 @@ def recipe_view(request, pk): 'bookmark_form': bookmark_form}) -@login_required() +@group_required('user') def books(request): book_list = [] @@ -106,7 +105,7 @@ def get_days_from_week(start, end): return days -@login_required() +@group_required('user') def meal_plan(request): js_week = datetime.now().strftime("%Y-W%V") if request.method == "POST": @@ -134,7 +133,7 @@ def meal_plan(request): return render(request, 'meal_plan.html', {'js_week': js_week, 'plan': plan, 'days': days, 'surrounding_weeks': surrounding_weeks}) -@login_required +@group_required('user') def shopping_list(request): markdown_format = True @@ -174,7 +173,7 @@ def shopping_list(request): return render(request, 'shopping_list.html', {'ingredients': ingredients, 'recipes': recipes, 'form': form, 'markdown_format': markdown_format}) -@login_required +@group_required('guest') def settings(request): try: up = request.user.userpreference