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
fomsemestre 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_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/<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
).