forked from ScoDoc/DocScoDoc
114 lines
4.3 KiB
Markdown
114 lines
4.3 KiB
Markdown
|
# 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:
|
||
|
```
|
||
|
@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`).
|