Droits
======

Le système de droit par défaut de Django n'est pas suffisamment granulaire pour les besoins de la Note Kfet 2020.
Un système personnalisé a donc été développé.

Il permet la création de Permission, qui autorise ou non a faire une action précise sur un ou des objets
de la base de données.

Plusieurs permissions peuvent être regroupé dans un Role.
Ce Role est relié a la responsabilité/fonction de la personne dans un Club. Ils sont valable au maximum tant que la
personne est adhérente du Club concerné, mais ce rôle peut être modifié/supprimé avant le terme.

Permission
----------

Une permission est un Model Django dont les principaux attributs sont :

* ``model`` : Le model sur lequel cette permission va s'appliquer
* ``type``  : Les différents types d'interaction sont : voir (``view``), modifier (``change``), ajouter (``add``)
  et supprimer (``delete``).
* ``query`` : Requête sur la cible, encodé en JSON, traduit en un Q object (cf `Query <#compilation-de-la-query>`_)
* ``field`` : le champ cible qui pourra être modifié. (tous les champs si vide)

Pour savoir si un⋅e utilisateur⋅rice a le droit sur un modèle ou non, la requête est compilée (voir ci-dessous) en un filtre
de requête dans la base de données, un objet de la classe ``Q`` (En SQL l'objet Q s'interprète comme tout ce qui suit
un ``WHERE ...`` Ils peuvent être combiné à l'aide d'opérateurs logiques. Plus d'information sur les Q object dans la
`documentation officielle <https://docs.djangoproject.com/fr/2.2/topics/db/queries/#complex-lookups-with-q-objects>`_.

Ce Q object sera donc utilisé pour savoir si l'instance que l'on veut modifier est concernée par notre permission.

Exception faite sur l'ajout d'objets : l'objet n'existant pas encore en base de données, il est ajouté puis supprimé
à la volée, en prenant soin de désactiver les signaux.

Compilation de la query
-----------------------

La query est enregistrée sous un format JSON, puis est traduite en requête ``Q`` récursivement en appliquant certains paramètres.
Le fonctionnemente de base des permission peux être décris avec les différents opérations :

+----------------+-----------------------------+-------------------------------------+
| opérations     | JSON                        | Q object                            |
+================+=============================+=====================================+
| ALL            | ``[]`` ou ``{}``            | ``.objects.get(Q(pk=F("pk"))``      |
+----------------+-----------------------------+-------------------------------------+
| ET             | ``["AND", Q_1, Q_2, ... ]`` | ``.objects.get(Q_1 & Q_2 & ...)``   |
+----------------+-----------------------------+-------------------------------------+
| OU             | ``["OR", Q_1, Q_2, ...]``   | ``.objects.get(Q_1 \| Q_2 \| ...)`` |
+----------------+-----------------------------+-------------------------------------+
| NON            | ``["NOT", Q_1]``            | ``.objects.get(~Q_1)``              |
+----------------+-----------------------------+-------------------------------------+
| key == value   | ``{key: value}``            | ``.objects.get(Q(key = value))``    |
+----------------+-----------------------------+-------------------------------------+


Vous n'avez rien compris ? Voilà des exemples :

Exemples
--------

* Permission sur le model ``User`` avec comme query:

  .. code:: js

    {"is_superuser": true}

  | si l'utilisateur⋅rice cible est un⋅e super utilisateur⋅rice.

* sur le model ``Note`` :

  .. code:: js

    {"pk":
      ["user","note", "pk"]
    }

  |  si l'identifiant de la note cible est l'identifiant de l'utilisateur⋅rice dont on regarde la permission.

* sur le model ``Transaction``:

  .. code:: js

    ["AND",
      {"source":
        ["user", "note"]},
      {"amount__lte":
        ["user", "note", "balance"]}
    ]

  | si la source est la note de l'utilisateur⋅rice et si le montant est inférieur à son solde.

* Sur le model ``Alias``

  .. code:: js

    ["OR",
      {"note__in":
        ["NoteUser", "objects",[
          "filter",{
            "user__membership__club__name": "Kfet"
          }],
          ["all"]
        ]},
      {"note__in":
        ["NoteClub", "objects", ["all"]]
      }
    ]

  | si l'alias appartient à une note de club ou s'il appartient à la note d'un⋅e utilisateur⋅rice membre du club Kfet.

* sur le model ``Transaction``

  .. code:: js

    ["AND",
      {"destination": ["club", "note"]},
      {"amount__lte":
         {"F": [
           "ADD",
           ["F", "source__balance"],
           2000]
         }
       }
    ]

  | si la destination est la note du club dont on est membre et si le montant est inférieur au solde de la source + 20 €,
    autrement dit le solde final est au-dessus de -20 €.


Masques de permissions
----------------------

Chaque permission est associée à un masque. À la connexion, l'utilisateur⋅rice choisit le masque de droits avec lequel iel
souhaite se connecter. Les masques sont ordonnés totalement, et l'utilisateur⋅rice aura effectivement une permission si iel est
en droit d'avoir la permission et si son masque est suffisamment haut.

Par exemple, si la permission de voir toutes les transactions est associée au masque « Droits note uniquement »,
se connecter avec le masque « Droits basiques » n'octroiera pas cette permission tandis que le masque « Tous mes droits » oui.

Signaux
-------

À chaque fois qu'un modèle est modifié, ajouté ou supprimé, les droits sont contrôlés. Si les droits ne sont pas
suffisants, une erreur est lancée. Pour ce qui est de la modification, on ne contrôle que les champs réellement
modifiés en comparant l'ancienne et la nouvelle instance.

Graphe des modèles
------------------

.. image:: ../_static/img/graphs/permission.svg
   :alt: Graphe de l'application permission
