diff --git a/docs/DevAPIPermissions.md b/docs/DevAPIPermissions.md new file mode 100644 index 00000000..9472ec84 --- /dev/null +++ b/docs/DevAPIPermissions.md @@ -0,0 +1,113 @@ +# Contrôle d'accès des fonctions de l'API + +## Rappels + +Dans ScoDoc, les permissions sont liées à des rôles. Un utilisateur a un ou plusieurs +rôles, dans un ou tous les départements (si le département est null, on +considère que le rôle est donnés dans tous les départements). + +Dans ScoDoc Web, toutes les routes sont liées à des départements + + /ScoDoc//Scolarite/ + +sauf la page d'accueil (`/ScoDoc/index`), les pages de configuration générale +(`ScoDoc/configuration`) et les pages "entreprises" (`ScoDoc/entreprises/`). + +L'API manipule des objets (formsemestres, modules, notes...) identifiés par leur +`id` qui est unique dans la base (note: les code INE et NIP ne sont pas des id, +et peuvent se retrouver dans plusieurs départements, notamment en cas de +transfert d'un étudiant d'un département à un autre). + +## Contrôle des permissions par l'API et départements +Ainsi, une route API comme `/partition/` n'est pas ambigüe. +Toutefois, ScoDoc doit déterminer si l'utilisateur a le droit d'accéder (ou de +modifier) cet objet. Pour cela, ScoDoc a besoin de connaitre le département. +C'est généralement assez simple: dans cet exemple, l'objet `partition` a une +relation avec `formsemestre`, lui même lié à son département: on écrit +`p.formsemestre.departement.acronym`. + +Cependant, le contrôle de l'accès est plus facile à exprimer (donc plus sûr, +moins de risque d'erreurs) avec un décorateur: on écrit typiquement les vues: +``` +@permission_required(Permission.ScoView) +def ma_vue( arg ): + ... +``` + +Comme nous l'avons dit, pour les vues Web (voir sources dans `app/view/*.py`), +le département est dans la route (URL): ainsi le tableau de bord d'un +fomsemestre est +``` +ScoDoc//Scolarite/Notes/formsemestre_status +``` +La vue s'écrit +``` +@bp.route("/formsemestre_status") +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_status(formsemestre_id:int): + ... +``` +Le décorateur `scodoc` (défini dans `app/scodoc/decorators.py`) récupère le +département présent dans la route et affecte deux attributs dans la requête +``` + g.scodoc_dept = "RT" # l'acronyme du dept + g.scodoc_dept_id = 5 # l'id du dept +``` + +Le décorateur suivant, `permission_required` peut ainsi vérifier que la +permission est bien accordée dans ce département. + +Pour l'API, on a deux routes: +``` +/ScoDoc/api/partition/ +``` +Dans ce cas, le décorateur `scodoc` ne récupère pas de département +(`g.scodoc_dept`est mis à `None`), et `permission_required`exige alors que la +permission soit accordé dans *tous les départements* (`dept`à `None`). + +Lorsque l'API est utilisée depuis une vue web de ScoDoc, donc par un utilisateur +ordinaire n'ayant de rôles que dans son (ou ses) départements, ce mécanisme +échoue. On propose donc une autre route, de la forme +``` + /ScoDoc//api/partition/ +``` +Les décorateurs fonctionnent alors bien. + +## Écriture d'une vue API +Il reste à la charge des fonctions de l'API d'effectuer la vérification que les +objets demandés sont bien dans le département donné par la route (point de +vigilance: risque de fuite de données si mal codé). Dans la plupart des cas, il +faut pour cela ajouter une jointure. par exemple, pour demander une partition, +on écrira non pas +``` +p = Partition.query.get(partition_id) +``` +mais plutôt +``` +p = Partition.query.filter_by(id=partition_id).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) +``` + +Écriture d'une vue de l'API accessible en mode web et API: +``` +@bp.route("/api_function/") +@api_web_bp.route("/api_function/") +@login_required +@scodoc +@permission_required(Permission.ScoView) +def api_function(arg: int): + """Une fonction quelconque de l'API""" + return jsonify({"current_user": current_user.to_dict(), "dept": g.scodoc_dept}) +``` + +## Fonctionnement interne du contrôle d'accès ScoDoc + +Les accès ScoDoc sont gérés avec `flask-login`. L'authentification est faite +soit par un cookie de session (web), soit par un jeton jwt (API). + +Ce décodage/contrôle est fait par la fonction +`app.auth.logic.load_user_from_request()`. + +En cas de refus (jeton ou cookie absent ou invalide), on a une redirection vers +la page de login (en mode web), ou un message d'erreur JSON 401 pour l'API +(voir `app.auth.logic.unauthorized_handler`).