added basic group permission system

This commit is contained in:
vabene1111 2020-04-26 17:21:44 +02:00
parent c7046bc705
commit ad467fae28
11 changed files with 157 additions and 53 deletions

View File

@ -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)

View File

@ -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)
]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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":

View File

@ -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)

View File

@ -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

View File

@ -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