mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			189 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			189 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | 
						|
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
						|
from collections import OrderedDict
 | 
						|
from datetime import date
 | 
						|
 | 
						|
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
						|
from django.contrib.auth.models import User
 | 
						|
from django.core.exceptions import PermissionDenied
 | 
						|
from django.db import transaction
 | 
						|
from django.db.models import Q
 | 
						|
from django.forms import HiddenInput
 | 
						|
from django.http import Http404
 | 
						|
from django.utils.translation import gettext_lazy as _
 | 
						|
from django.views.generic import UpdateView, TemplateView, CreateView
 | 
						|
from django_tables2 import MultiTableMixin
 | 
						|
from member.models import Membership
 | 
						|
 | 
						|
from .backends import PermissionBackend
 | 
						|
from .models import Role
 | 
						|
from .tables import RightsTable, SuperuserTable
 | 
						|
 | 
						|
 | 
						|
class ProtectQuerysetMixin:
 | 
						|
    """
 | 
						|
    This is a View class decorator and not a proper View class.
 | 
						|
    Ensure that the user has the right to see or update objects.
 | 
						|
    Display 404 error if the user can't see an object, remove the fields the user can't
 | 
						|
    update on an update form (useful if the user can't change only specified fields).
 | 
						|
    """
 | 
						|
    def get_queryset(self, filter_permissions=True, **kwargs):
 | 
						|
        qs = super().get_queryset(**kwargs)
 | 
						|
        return qs.filter(PermissionBackend.filter_queryset(self.request, qs.model, "view")).distinct()\
 | 
						|
            if filter_permissions else qs
 | 
						|
 | 
						|
    def get_object(self, queryset=None):
 | 
						|
        try:
 | 
						|
            return super().get_object(queryset)
 | 
						|
        except Http404 as e:
 | 
						|
            if self.get_queryset(filter_permissions=False).count() == self.get_queryset().count():
 | 
						|
                raise e
 | 
						|
            raise PermissionDenied()
 | 
						|
 | 
						|
    def get_form(self, form_class=None):
 | 
						|
        form = super().get_form(form_class)
 | 
						|
 | 
						|
        if not isinstance(self, UpdateView):
 | 
						|
            return form
 | 
						|
 | 
						|
        # If we are in an UpdateView, we display only the fields the user has right to see.
 | 
						|
        # No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
 | 
						|
        # a custom request.
 | 
						|
        # We could also delete the field, but some views might be affected.
 | 
						|
        meta = form.instance._meta
 | 
						|
        for key in form.base_fields:
 | 
						|
            if not PermissionBackend.check_perm(self.request,
 | 
						|
                                                f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
 | 
						|
                form.fields[key].widget = HiddenInput()
 | 
						|
 | 
						|
        return form
 | 
						|
 | 
						|
    @transaction.atomic
 | 
						|
    def form_valid(self, form):
 | 
						|
        """
 | 
						|
        Submit the form, if the page is a FormView.
 | 
						|
        If a PermissionDenied exception is raised, catch the error and display it at the top of the form.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            return super().form_valid(form)
 | 
						|
        except PermissionDenied:
 | 
						|
            if isinstance(self, UpdateView):
 | 
						|
                form.add_error(None, _("You don't have the permission to update this instance of the model \"{model}\""
 | 
						|
                                       " with these parameters. Please correct your data and retry.")
 | 
						|
                               .format(model=self.model._meta.verbose_name))
 | 
						|
            else:
 | 
						|
                form.add_error(None, _("You don't have the permission to create an instance of the model \"{model}\""
 | 
						|
                                       " with these parameters. Please correct your data and retry.")
 | 
						|
                               .format(model=self.model._meta.verbose_name))
 | 
						|
            return self.form_invalid(form)
 | 
						|
 | 
						|
 | 
						|
class ProtectedCreateView(LoginRequiredMixin, CreateView):
 | 
						|
    """
 | 
						|
    Extends a CreateView to check is the user has the right to create a sample instance of the given Model.
 | 
						|
    If not, a 403 error is displayed.
 | 
						|
    """
 | 
						|
 | 
						|
    def get_sample_object(self):  # pragma: no cover
 | 
						|
        """
 | 
						|
        return a sample instance of the Model.
 | 
						|
        It should be valid (can be stored properly in database), but must not collide with existing data.
 | 
						|
        """
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def dispatch(self, request, *args, **kwargs):
 | 
						|
        # Check that the user is authenticated before that he/she has the permission to access here
 | 
						|
        if not request.user.is_authenticated:
 | 
						|
            return self.handle_no_permission()
 | 
						|
 | 
						|
        model_class = self.model
 | 
						|
        # noinspection PyProtectedMember
 | 
						|
        app_label, model_name = model_class._meta.app_label, model_class._meta.model_name.lower()
 | 
						|
        perm = app_label + ".add_" + model_name
 | 
						|
        if not PermissionBackend.check_perm(request, perm, self.get_sample_object()):
 | 
						|
            raise PermissionDenied(_("You don't have the permission to add an instance of model "
 | 
						|
                                     "{app_label}.{model_name}.").format(app_label=app_label, model_name=model_name))
 | 
						|
        return super().dispatch(request, *args, **kwargs)
 | 
						|
 | 
						|
 | 
						|
class RightsView(MultiTableMixin, TemplateView):
 | 
						|
    template_name = "permission/all_rights.html"
 | 
						|
    extra_context = {"title": _("Rights")}
 | 
						|
 | 
						|
    tables = [
 | 
						|
        lambda data: RightsTable(data, prefix="clubs-"),
 | 
						|
        lambda data: SuperuserTable(data, prefix="superusers-"),
 | 
						|
    ]
 | 
						|
 | 
						|
    def get_tables_data(self):
 | 
						|
        special_memberships = Membership.objects.filter(
 | 
						|
            date_start__lte=date.today(),
 | 
						|
            date_end__gte=date.today(),
 | 
						|
        ).filter(roles__in=Role.objects.filter((~(Q(name="Adhérent⋅e BDE")
 | 
						|
                                                  | Q(name="Adhérent⋅e Kfet")
 | 
						|
                                                  | Q(name="Membre de club")
 | 
						|
                                                  | Q(name="Bureau de club"))
 | 
						|
                                                & Q(weirole__isnull=True))))\
 | 
						|
            .order_by("club__name", "user__last_name")\
 | 
						|
            .distinct().all()
 | 
						|
        return [
 | 
						|
            special_memberships,
 | 
						|
            User.objects.filter(is_superuser=True).order_by("last_name"),
 | 
						|
        ]
 | 
						|
 | 
						|
    def get_context_data(self, **kwargs):
 | 
						|
        context = super().get_context_data(**kwargs)
 | 
						|
 | 
						|
        context["title"] = _("All rights")
 | 
						|
        roles = Role.objects.all()
 | 
						|
        context["roles"] = roles
 | 
						|
        if self.request.user.is_authenticated:
 | 
						|
            active_memberships = Membership.objects.filter(user=self.request.user,
 | 
						|
                                                           date_start__lte=date.today(),
 | 
						|
                                                           date_end__gte=date.today()).all()
 | 
						|
        else:
 | 
						|
            active_memberships = Membership.objects.none()
 | 
						|
 | 
						|
        for role in roles:
 | 
						|
            role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
 | 
						|
 | 
						|
        if self.request.user.is_authenticated:
 | 
						|
            tables = context["tables"]
 | 
						|
            for name, table in zip(["special_memberships_table", "superusers"], tables):
 | 
						|
                context[name] = table
 | 
						|
 | 
						|
        return context
 | 
						|
 | 
						|
 | 
						|
class ScopesView(LoginRequiredMixin, TemplateView):
 | 
						|
    template_name = "permission/scopes.html"
 | 
						|
 | 
						|
    def get_context_data(self, **kwargs):
 | 
						|
        context = super().get_context_data(**kwargs)
 | 
						|
 | 
						|
        from oauth2_provider.models import Application
 | 
						|
        from .scopes import PermissionScopes
 | 
						|
 | 
						|
        oidc = False
 | 
						|
        context["scopes"] = {}
 | 
						|
        for app in Application.objects.filter(user=self.request.user).all():
 | 
						|
            available_scopes = PermissionScopes().get_available_scopes(app)
 | 
						|
            context["scopes"][app] = OrderedDict()
 | 
						|
            all_scopes = PermissionScopes().get_all_scopes(scopes=available_scopes)
 | 
						|
            scopes = {}
 | 
						|
            for scope in available_scopes:
 | 
						|
                scopes[scope] = all_scopes[scope]
 | 
						|
            # remove OIDC scope for sort
 | 
						|
            if 'openid' in scopes:
 | 
						|
                del scopes['openid']
 | 
						|
                oidc = True
 | 
						|
            items = [(k, v) for (k, v) in scopes.items()]
 | 
						|
            items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
 | 
						|
            # add oidc if necessary
 | 
						|
            if oidc:
 | 
						|
                items.append(('openid', PermissionScopes().get_all_scopes(scopes=['openid'])['openid']))
 | 
						|
            for k, v in items:
 | 
						|
                context["scopes"][app][k] = v
 | 
						|
 | 
						|
        return context
 |