mirror of https://gitlab.crans.org/bde/nk20
Protect views from viewing if the user has no right to view an object
This commit is contained in:
parent
e461d70b14
commit
730d37c620
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
|
@ -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,21 +74,29 @@ 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">
|
||||||
<li class="nav-item active">
|
{% if "note.transactiontemplate"|not_empty_model_list %}
|
||||||
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
|
<li class="nav-item active">
|
||||||
</li>
|
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
|
||||||
<li class="nav-item active">
|
</li>
|
||||||
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
|
{% endif %}
|
||||||
</li>
|
{% if "member.club"|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="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item active">
|
{% endif %}
|
||||||
<a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
|
{% if "activity.activity"|not_empty_model_list %}
|
||||||
</li>
|
<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="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
|
</li>
|
||||||
</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>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
|
Loading…
Reference in New Issue