DocAssiduites/docs/DevAPIPermissions.md

4.3 KiB

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/<str:dept_acronym>/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/<int:partition_id> 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 formsemestre est

ScoDoc/<str:dept_acronym>/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/<int:partition_id>

Dans ce cas, le décorateur scodoc ne récupère pas de département (g.scodoc_deptest mis à None), et permission_requiredexige 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/<str:dept_acronym>/api/partition/<int:partition_id>

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:

@api_bp.route("/api_function/<int:arg>")
@api_web_bp.route("/api_function/<int:arg>")
@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).