documentation: tutorial about rights management

This commit is contained in:
Antoine Bartuccio 2019-08-14 18:46:33 +02:00
parent ed68c2cb38
commit db10f7b963
Signed by: klmp200
GPG Key ID: E7245548C53F904B
4 changed files with 220 additions and 37 deletions

View File

@ -18,14 +18,6 @@
:target: https://sith-ae.readthedocs.io/?badge=latest :target: https://sith-ae.readthedocs.io/?badge=latest
:alt: documentation Status :alt: documentation Status
.. body
.. role:: bash(code)
:language: bash
Sith AE
=======
| **Website** is available here https://ae.utbm.fr/. | **Website** is available here https://ae.utbm.fr/.
| **Documentation** is available here https://sith-ae.readthedocs.io/. | **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. This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.
Controlling the rights 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.
~~~~~~~~~~~~~~~~~~~~~~
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.

View File

@ -81,18 +81,60 @@ def internal_servor_error(request):
def can_edit_prop(obj, user): 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): if obj is None or user.is_owner(obj):
return True return True
return False return False
def can_edit(obj, user): 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): if obj is None or user.can_edit(obj):
return True return True
return can_edit_prop(obj, user) return can_edit_prop(obj, user)
def can_view(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): if obj is None or user.can_view(obj):
return True return True
return can_edit(obj, user) 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 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 of the following mixin
:raises: PermissionDenied
""" """
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
@ -123,6 +167,8 @@ class CanEditPropMixin(View):
to only the owner group of the given object. 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 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 object's owner_group
:raises: PermissionDenied
""" """
def dispatch(self, request, *arg, **kwargs): 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 This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
object object
:raises: PermissionDenied
""" """
def dispatch(self, request, *arg, **kwargs): 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 This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
the object the object
:raises: PermissionDenied
""" """
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
@ -206,6 +256,8 @@ class CanViewMixin(View):
class FormerSubscriberMixin(View): class FormerSubscriberMixin(View):
""" """
This view check if the user was at least an old subscriber This view check if the user was at least an old subscriber
:raises: PermissionDenied
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -217,6 +269,8 @@ class FormerSubscriberMixin(View):
class UserIsLoggedMixin(View): class UserIsLoggedMixin(View):
""" """
This view check if the user is logged This view check if the user is logged
:raises: PermissionDenied
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):

View File

@ -7,7 +7,6 @@ Bienvenue sur la documentation du Sith de l'AE
============================================== ==============================================
.. include:: ../README.rst .. include:: ../README.rst
:end-before: body
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@ -28,6 +27,12 @@ Bienvenue sur la documentation du Sith de l'AE
start/devtools start/devtools
.. toctree::
:maxdepth: 2
:caption: La surcouche "Site AE"
overlay/rights
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
:caption: Documentation des apps :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 <TW_Skia/Rapport.pdf>` * :download:`Rapport de la TW de Skia sur la création du site <TW_Skia/Rapport.pdf>`
* :download:`Rapport sur la TO de Skia et LoJ <TO_Skia_LoJ/Rapport.pdf>` * :download:`Rapport sur la TO de Skia et LoJ <TO_Skia_LoJ/Rapport.pdf>`
* :download:`Manuel du service E-transactions <Etransaction/Manuel_Integration_E-transactions_Internet_V6.6_FR.pdf>` * :download:`Manuel du service E-transactions <Etransaction/Manuel_Integration_E-transactions_Internet_V6.6_FR.pdf>`
* :download:`Guide de trésorerie <Guide de Tresorerie.pdf>` * :download:`Guide de trésorerie <Guide de Tresorerie.pdf>`
.. include:: ../README.rst
:start-after: body

158
doc/overlay/rights.rst Normal file
View File

@ -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) %}
<a href="{{ url('club:tools', club_id=club.id) }}">{{ club }}</a>
{% 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