Protect views from viewing if the user has no right to view an object

This commit is contained in:
Yohann D'ANELLO 2020-03-19 02:26:06 +01:00
parent e461d70b14
commit 730d37c620
9 changed files with 116 additions and 35 deletions

View File

@ -13,7 +13,7 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
def get_queryset(self):
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, model, "view"))
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
@ -23,4 +23,4 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, model, "view"))

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db.models import Q, F
@ -15,7 +16,8 @@ class PermissionBackend(ModelBackend):
supports_anonymous_user = False
supports_inactive_user = False
def permissions(self, user):
@staticmethod
def permissions(user):
for membership in Membership.objects.filter(user=user).all():
if not membership.valid() or membership.roles is None:
continue
@ -37,12 +39,13 @@ class PermissionBackend(ModelBackend):
)
yield permission
def filter_queryset(self, user, model, type, field=None):
@staticmethod
def filter_queryset(user, model, t, field=None):
"""
Filter a queryset by considering the permissions of a given user.
:param user: The owner of the permissions that are fetched
:param model: The concerned model of the queryset
:param type: The type of modification (view, add, change, delete)
:param t: The type of modification (view, add, change, delete)
:param field: The field of the model to test, if concerned
:return: A query that corresponds to the filter to give to a queryset
"""
@ -51,12 +54,15 @@ class PermissionBackend(ModelBackend):
# Superusers have all rights
return Q()
if not isinstance(model, ContentType):
model = ContentType.objects.get_for_model(model)
# Never satisfied
query = Q(pk=-1)
for perm in self.permissions(user):
if field and field != perm.field:
for perm in PermissionBackend.permissions(user):
if perm.field and field != perm.field:
continue
if perm.model != model or perm.type != type:
if perm.model != model or perm.type != t:
continue
query = query | perm.query
return query

View File

@ -23,6 +23,7 @@ from note.forms import AliasForm, ImageForm
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable, AliasTable
from .backends import PermissionBackend
from .filters import UserFilter, UserFilterFormHelper
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
@ -120,6 +121,9 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context_object_name = "user_object"
template_name = "member/profile_detail.html"
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = context['user_object']
@ -147,7 +151,7 @@ class UserListView(LoginRequiredMixin, SingleTableView):
formhelper_class = UserFilterFormHelper
def get_queryset(self, **kwargs):
qs = super().get_queryset()
qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
self.filter = self.filter_class(self.request.GET, queryset=qs)
self.filter.form.helper = self.formhelper_class()
return self.filter.qs
@ -296,7 +300,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
if not self.request.user.is_authenticated:
return User.objects.none()
qs = User.objects.all()
qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all()
if self.q:
qs = qs.filter(username__regex="^" + self.q)
@ -327,11 +331,17 @@ class ClubListView(LoginRequiredMixin, SingleTableView):
model = Club
table_class = ClubTable
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = context["club"]
@ -350,6 +360,11 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView):
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")
| PermissionBackend.filter_queryset(self.request.user, Membership,
"change"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()

View File

@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from member.backends import PermissionBackend
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
@ -70,7 +71,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
Parse query and apply filters.
:return: The filtered set of requested notes
"""
queryset = super().get_queryset()
queryset = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Note, "view"))
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
@ -110,7 +111,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
:return: The filtered set of requested aliases
"""
queryset = super().get_queryset()
queryset = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(

View File

@ -129,13 +129,14 @@ class Transaction(PolymorphicModel):
models.Index(fields=['destination']),
]
def post_save(self, *args, **kwargs):
def save(self, *args, **kwargs):
"""
When saving, also transfer money between two notes
"""
if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered
super().save(*args, **kwargs)
return
created = self.pk is None

View File

@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView
from django_tables2 import SingleTableView
from member.backends import PermissionBackend
from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial
from .models.transactions import SpecialTransaction
@ -18,16 +19,18 @@ from .tables import HistoryTable
class TransactionCreate(LoginRequiredMixin, SingleTableView):
"""
Show transfer page
TODO: If user have sufficient rights, they can transfer from an other note
"""
queryset = Transaction.objects.order_by("-id").all()[:50]
template_name = "note/transaction_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(PermissionBackend
.filter_queryset(self.request.user, Transaction, "view")) \
.order_by("-id").all()[:50]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
@ -117,21 +120,26 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
"""
Consume
"""
queryset = Transaction.objects.order_by("-id").all()[:50]
template_name = "note/conso_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(PermissionBackend
.filter_queryset(self.request.user, Transaction, "view")) \
.order_by("-id").all()[:50]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
from django.db.models import Count
buttons = TransactionTemplate.objects.filter(display=True) \
.annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name')
buttons = TransactionTemplate.objects.filter(PermissionBackend()
.filter_queryset(self.request.user, TransactionTemplate, "view")) \
.filter(display=True).annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name')
context['transaction_templates'] = buttons
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
context['title'] = _("Consumptions")

View File

View File

@ -0,0 +1,42 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from logs.middlewares import get_current_authenticated_user
from django import template
from member.backends import PermissionBackend
def has_perm(value):
return get_current_authenticated_user().has_perm(value)
@stringfilter
def not_empty_model_list(model_name):
user = get_current_authenticated_user()
if user.is_superuser:
return True
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view"))
return qs.exists()
@stringfilter
def not_empty_model_change_list(model_name):
user = get_current_authenticated_user()
if user.is_superuser:
return True
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
return qs.exists()
register = template.Library()
register.filter('has_perm', has_perm)
register.filter('not_empty_model_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list)

View File

@ -1,4 +1,4 @@
{% load static i18n pretty_money static getenv %}
{% load static i18n pretty_money static getenv perms %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
@ -74,18 +74,26 @@ SPDX-License-Identifier: GPL-3.0-or-later
</button>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav">
{% if "note.transactiontemplate"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li>
{% endif %}
{% if "member.club"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
</li>
{% endif %}
{% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li>
{% endif %}
{% if "note.transactiontemplate"|not_empty_model_change_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
</li>