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

View File

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

View File

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

View File

@ -129,13 +129,14 @@ class Transaction(PolymorphicModel):
models.Index(fields=['destination']), models.Index(fields=['destination']),
] ]
def post_save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
When saving, also transfer money between two notes When saving, also transfer money between two notes
""" """
if self.source.pk == self.destination.pk: if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered # When source == destination, no money is transfered
super().save(*args, **kwargs)
return return
created = self.pk is None 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.views.generic import CreateView, ListView, UpdateView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from member.backends import PermissionBackend
from .forms import TransactionTemplateForm from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial
from .models.transactions import SpecialTransaction from .models.transactions import SpecialTransaction
@ -18,16 +19,18 @@ from .tables import HistoryTable
class TransactionCreate(LoginRequiredMixin, SingleTableView): class TransactionCreate(LoginRequiredMixin, SingleTableView):
""" """
Show transfer page 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" template_name = "note/transaction_form.html"
# Transaction history table # Transaction history table
table_class = HistoryTable table_class = HistoryTable
table_pagination = {"per_page": 50} 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): def get_context_data(self, **kwargs):
""" """
Add some context variables in template such as page title Add some context variables in template such as page title
@ -117,21 +120,26 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
""" """
Consume Consume
""" """
queryset = Transaction.objects.order_by("-id").all()[:50]
template_name = "note/conso_form.html" template_name = "note/conso_form.html"
# Transaction history table # Transaction history table
table_class = HistoryTable table_class = HistoryTable
table_pagination = {"per_page": 50} 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): def get_context_data(self, **kwargs):
""" """
Add some context variables in template such as page title Add some context variables in template such as page title
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
from django.db.models import Count from django.db.models import Count
buttons = TransactionTemplate.objects.filter(display=True) \ buttons = TransactionTemplate.objects.filter(PermissionBackend()
.annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') .filter_queryset(self.request.user, TransactionTemplate, "view")) \
.filter(display=True).annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name')
context['transaction_templates'] = buttons context['transaction_templates'] = buttons
context['most_used'] = buttons.order_by('-clicks', 'name')[:10] context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
context['title'] = _("Consumptions") 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 %} {% comment %}
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
@ -74,18 +74,26 @@ SPDX-License-Identifier: GPL-3.0-or-later
</button> </button>
<div class="collapse navbar-collapse" id="navbarNavDropdown"> <div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if "note.transactiontemplate"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li> </li>
{% endif %}
{% if "member.club"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
</li> </li>
{% endif %}
{% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li> </li>
{% endif %}
{% if "note.transactiontemplate"|not_empty_model_change_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
</li> </li>
{% endif %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
</li> </li>