From db10f7b96323edbcb9f6a729d91312bce543b3d9 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Wed, 14 Aug 2019 18:46:33 +0200 Subject: [PATCH] documentation: tutorial about rights management --- README.rst | 33 +-------- core/views/__init__.py | 54 ++++++++++++++ doc/index.rst | 12 ++-- doc/overlay/rights.rst | 158 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 doc/overlay/rights.rst diff --git a/README.rst b/README.rst index bec0058a..ad996ba1 100644 --- a/README.rst +++ b/README.rst @@ -18,14 +18,6 @@ :target: https://sith-ae.readthedocs.io/?badge=latest :alt: documentation Status -.. body - -.. role:: bash(code) - :language: bash - -Sith AE -======= - | **Website** is available here https://ae.utbm.fr/. | **Documentation** is available here https://sith-ae.readthedocs.io/. @@ -34,27 +26,4 @@ Sith AE This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details. -Controlling the rights -~~~~~~~~~~~~~~~~~~~~~~ - -When you need to protect an object, there are three levels: - - * Editing the object properties - * Editing the object various values - * Viewing the object - -Now you have many solutions in your model: - - * You can define a :bash:`is_owned_by(self, user)`, a :bash:`can_be_edited_by(self, user)`, and/or a :bash:`can_be_viewed_by(self, user)` method, each returning True is the user passed can edit/view the object, False otherwise. - - * This allows you to make complex request when the group solution is not powerful enough. - * It's useful too when you want to define class-wide permissions, e.g. the club members, that are viewable only for Subscribers. - - * You can add an :bash:`owner_group` field, as a ForeignKey to Group. Second is an :bash:`edit_groups` field, as a ManyToMany to Group, and third is a :bash:`view_groups`, same as for edit. - - - - -Finally, when building a class based view, which is highly advised, you just have to inherit it from CanEditPropMixin, -CanEditMixin, or CanViewMixin, which are located in core.views. Your view will then be protected using either the -appropriate group fields, or the right method to check user permissions. +The code is written only in english but the documentation is written in french for comprehension reasons. It will not be written in english because it's too much work. diff --git a/core/views/__init__.py b/core/views/__init__.py index 41bd5eee..f3783d31 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -81,18 +81,60 @@ def internal_servor_error(request): def can_edit_prop(obj, user): + """ + :param obj: Object to test for permission + :param user: core.models.User to test permissions against + :return: if user is authorized to edit object properties + :rtype: bool + + :Example: + + .. code-block:: python + + if not can_edit_prop(self.object ,request.user): + raise PermissionDenied + + """ if obj is None or user.is_owner(obj): return True return False def can_edit(obj, user): + """ + :param obj: Object to test for permission + :param user: core.models.User to test permissions against + :return: if user is authorized to edit object + :rtype: bool + + :Example: + + .. code-block:: python + + if not can_edit(self.object ,request.user): + raise PermissionDenied + + """ if obj is None or user.can_edit(obj): return True return can_edit_prop(obj, user) def can_view(obj, user): + """ + :param obj: Object to test for permission + :param user: core.models.User to test permissions against + :return: if user is authorized to see object + :rtype: bool + + :Example: + + .. code-block:: python + + if not can_view(self.object ,request.user): + raise PermissionDenied + + """ if obj is None or user.can_view(obj): return True return can_edit(obj, user) @@ -102,6 +144,8 @@ class CanCreateMixin(View): """ This view is made to protect any child view that would create an object, and thus, that can not be protected by any of the following mixin + + :raises: PermissionDenied """ def dispatch(self, request, *arg, **kwargs): @@ -123,6 +167,8 @@ class CanEditPropMixin(View): to only the owner group of the given object. In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the object's owner_group + + :raises: PermissionDenied """ def dispatch(self, request, *arg, **kwargs): @@ -150,6 +196,8 @@ class CanEditMixin(View): """ This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the object + + :raises: PermissionDenied """ def dispatch(self, request, *arg, **kwargs): @@ -177,6 +225,8 @@ class CanViewMixin(View): """ This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of the object + + :raises: PermissionDenied """ def dispatch(self, request, *arg, **kwargs): @@ -206,6 +256,8 @@ class CanViewMixin(View): class FormerSubscriberMixin(View): """ This view check if the user was at least an old subscriber + + :raises: PermissionDenied """ def dispatch(self, request, *args, **kwargs): @@ -217,6 +269,8 @@ class FormerSubscriberMixin(View): class UserIsLoggedMixin(View): """ This view check if the user is logged + + :raises: PermissionDenied """ def dispatch(self, request, *args, **kwargs): diff --git a/doc/index.rst b/doc/index.rst index 0724d75f..99a1d47f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -7,7 +7,6 @@ Bienvenue sur la documentation du Sith de l'AE ============================================== .. include:: ../README.rst - :end-before: body .. toctree:: :maxdepth: 2 @@ -28,6 +27,12 @@ Bienvenue sur la documentation du Sith de l'AE start/devtools +.. toctree:: + :maxdepth: 2 + :caption: La surcouche "Site AE" + + overlay/rights + .. toctree:: :maxdepth: 3 :caption: Documentation des apps @@ -76,7 +81,4 @@ Documents téléchargeables * :download:`Rapport de la TW de Skia sur la création du site ` * :download:`Rapport sur la TO de Skia et LoJ ` * :download:`Manuel du service E-transactions ` -* :download:`Guide de trésorerie ` - -.. include:: ../README.rst - :start-after: body +* :download:`Guide de trésorerie ` \ No newline at end of file diff --git a/doc/overlay/rights.rst b/doc/overlay/rights.rst new file mode 100644 index 00000000..50b2f50e --- /dev/null +++ b/doc/overlay/rights.rst @@ -0,0 +1,158 @@ +La gestion des droits +===================== + +La gestion des droits dans l'association étant très complexe, le système de permissions intégré dans Django ne suffisait pas, il a donc fallu créer et intégrer le notre. + +La gestion des permissions se fait directement sur un modèle, il existe trois niveaux de permission : + +* Édition des propriétés de l'objet +* Édition de certaines valeurs l'objet +* Voir l'objet + +Protéger un modèle +------------------ + +Lors de l'écriture d'un modèle, il est très simple de définir des permissions. Celle-ci peuvent être basées sur des groupes et/ou sur des fonctions personnalisées. + +Nous allons présenter ici les deux techniques. Dans un premier temps nous allons protéger une classe Article par groupes. + +.. code-block:: python + + from django.db import models + from django.conf import settings + from django.utils.translation import ugettext_lazy as _ + + from core.views import * + from core.models import User, Group + + # Utilisation de la protection par groupe + class Article(models.Model): + + title = models.CharField(_("title"), max_length=100) + content = models.TextField(_("content")) + + # Ne peut pas être une liste + # Groupe possédant l'objet + # Donne les droits d'édition des propriétés de l'objet + owner_group = models.ForeignKey( + Group, related_name="owned_articles", default=settings.SITH_GROUP_ROOT_ID + ) + + # Doit être une liste + # Tous les groupes qui seront ajouté dans ce champ auront les droits d'édition de l'objet + edit_group = models.ManyToManyField( + edit_groups = models.ManyToManyField( + Group, + related_name="editable_articles", + verbose_name=_("edit groups"), + blank=True, + ) + ) + + # Doit être une liste + # Tous les groupes qui seront ajouté dans ce champ auront les droits de vue de l'objet + view_groups = models.ManyToManyField( + Group, + related_name="viewable_articles", + verbose_name=_("view groups"), + blank=True, + ) + +Voici maintenant comment faire en définissant des fonctions personnalisées. Cette deuxième solution permet, dans le cas où la première n'est pas assez puissante, de créer des permissions complexes et fines. Attention à bien les rendre lisibles et de bien documenter. + +.. code-block:: python + + from django.db import models + from django.utils.translation import ugettext_lazy as _ + + from core.views import * + from core.models import User, Group + + # Utilisation de la protection par fonctions + class Article(models.Model): + + title = models.CharField(_("title"), max_length=100) + content = models.TextField(_("content")) + + # Donne ou non les droits d'édition des propriétés de l'objet + # Un utilisateur dans le bureau AE aura tous les droits sur cet objet + def is_owned_by(self, user): + return user.is_board_member + + # Donne ou non les droits d'édition de l'objet + # L'objet ne sera modifiable que par un utilisateur cotisant + def can_be_edited_by(self, user): + return user.is_subscribed + + # Donne ou non les droits de vue de l'objet + # Ici, l'objet n'est visible que par un utilisateur connecté + def can_be_viewed_by(self, user): + return not user.user.is_anonymous + +.. note:: + + Si un utilisateur est autorisé à un niveau plus élevé que celui auquel il est testé, le système le détectera automatiquement et les droits lui seront accordé. Par défaut, les seuls utilisateur ayant des droits quelconques sont les *superuser* de Django et les membres du bureau AE qui sont définis comme *owner*. + +Appliquer les permissions +------------------------- + +Dans un template +~~~~~~~~~~~~~~~~ + +Il existe trois fonctions de base sur lesquelles reposent les vérifications de permission. Elles sont disponibles dans le contexte par défaut du moteur de template et peuvent être utilisées à tout moment. + +Tout d'abord, voici leur documentation et leur prototype. + +.. autofunction:: core.views.can_edit_prop +.. autofunction:: core.views.can_edit +.. autofunction:: core.views.can_view + +Voici un exemple d'utilisation dans un template : + +.. code-block:: html+jinja + + {# ... #} + {% if can_edit(club, user) %} + {{ club }} + {% endif %} + +Dans une vue +~~~~~~~~~~~~ + +Généralement, les vérifications de droits dans les templates se limitent au liens à afficher puisqu'il ne faut pas mettre de logique autre que d'affichage à l'intérieur. C'est donc généralement au niveau des vues que cela a lieu. + +Notre système s'appuie sur un système de mixin à hériter lors de la création d'une vue basée sur une classe. Ces mixins ne sont compatibles qu'avec les classes récupérant un objet ou une liste d'objet. Dans le cas d'un seul objet, une permission refusée est levée lorsque l'utilisateur n'as pas le droit de visionner la page. Dans le d'une liste d'objet, le mixin filtre les objets non autorisés et si aucun ne l'est l'utilisateur recevra une liste vide d'objet. + +Voici un exemple d'utilisation en reprenant l'objet Article crée précédemment : + +.. code-block:: python + + from django.views.generic import CreateView, ListView + + from core.views import CanViewMixin, CanCreateMixin + + from .models import Article + + ... + + # Il est important de mettre le mixin avant la classe héritée de Django + # L'héritage multiple se fait de droite à gauche et les mixins ont besoin + # d'une classe de base pour fonctionner correctement. + class ArticlesListView(CanViewMixin, ListView): + + model = Article # On base la vue sur le modèle Article + ... + + # Même chose pour une vue de création de l'objet Article + class ArticlesCreateView(CanCreateMixin, CreateView): + + model = Article + +Le système de permissions de propose plusieurs mixins différents, les voici dans leur intégralité. + +.. autoclass:: core.views.CanCreateMixin +.. autoclass:: core.views.CanEditPropMixin +.. autoclass:: core.views.CanEditMixin +.. autoclass:: core.views.CanViewMixin +.. autoclass:: core.views.FormerSubscriberMixin +.. autoclass:: core.views.UserIsLoggedMixin \ No newline at end of file