1. Reprenez le contrôleur complet du Jour 2
Remplacez le contenu de training_notes.py par cette version cohérente. Les deux routes exigent une session Odoo connectée. Elles utilisent l'environnement normal de la requête et ne demandent aucune élévation de privilèges.
from odoo import http
from odoo.http import request
class TrainingNotesController(http.Controller):
@http.route("/training/notes", type="http", auth="user", methods=["GET"])
def notes_page(self):
user = request.env.user
notes = request.env["training.note"].search(
[("user_id", "=", user.id)],
order="id desc",
)
return request.render("training_hello.training_notes_page", {
"notes": notes,
})
@http.route("/training/notes", type="http", auth="user", methods=["POST"])
def create_note(self, **kwargs):
title = (kwargs.get("title") or "").strip()
body = kwargs.get("body") or ""
if title:
request.env["training.note"].create({
"title": title,
"body": body,
})
return request.redirect("/training/notes", code=303)Pour le GET, le contrôleur prend user.id depuis request.env.user et filtre avec lui. Alex et Sam demandent donc la même URL, mais n'obtiennent pas la même liste.
Pour le POST, kwargs peut contenir les paramètres envoyés par le formulaire, mais le contrôleur ne lit que title et body. Comme il ne fournit pas user_id, le défaut du modèle utilise env.user.
2. Appliquez le même filtre à l'API
La route API utilise une clé Bearer et le même environnement authentifié. Elle ne lit ni user_id ni un identifiant de note envoyé par le navigateur pour choisir le propriétaire.
@http.route("/training/api/notes", type="http", auth="bearer", methods=["GET"])
def list_notes(self):
user = request.env.user
notes = request.env["training.note"].search_read(
[("user_id", "=", user.id)],
fields=["id", "title", "body"],
order="id desc",
)
return request.make_json_response({"notes": notes})Ne mettez pas sudo() dans cette route. Sans le filtre, une route qui renverrait toutes les notes créerait un IDOR ; avec sudo(), elle contournerait aussi les droits Odoo.
3. Affichez le propriétaire dans le gabarit QWeb
Gardez le formulaire et la liste dans le même gabarit. Cet extrait complet utilise uniquement les champs réels de la note nécessaires ici : title, body et user_id.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="training_notes_page">
<h1>Notes de formation</h1>
<form action="/training/notes" method="post">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<label>
Titre
<input name="title" required="required"/>
</label>
<label>
Contenu
<textarea name="body"></textarea>
</label>
<button type="submit">Enregistrer</button>
</form>
<ul>
<li t-foreach="notes" t-as="note">
<strong><t t-out="note.title"/></strong>
<small>
Propriétaire : <t t-out="note.user_id.name"/>
</small>
<p><t t-out="note.body"/></p>
</li>
</ul>
</template>
</odoo>Le gabarit ne choisit pas les notes autorisées. Il parcourt seulement les enregistrements déjà obtenus par le contrôleur. note.user_id.name suit la relation Many2one et affiche le nom du propriétaire choisi par Odoo.
4. Conservez la protection CSRF du formulaire
Le champ caché csrf_token contient un jeton produit par Odoo pour cette session. Lors du POST, Odoo le vérifie afin de refuser l'envoi d'un formulaire forgé depuis un autre site. Cette protection du formulaire complète l'authentification et les droits de l'ORM ; elle ne fournit pas le propriétaire de la note.
N'ajoutez aucun contrôle caché ou visible pour le propriétaire. Une personne peut modifier le HTML avant l'envoi. L'identité fiable vient de la session validée par auth="user", puis de request.env.user côté serveur.
5. Rechargez Python et XML
- Action qui redémarre Odoo : redémarrez le processus local pour charger le contrôleur Python, puis attendez que les journaux indiquent que le serveur est prêt.
- Action qui modifie la base d'exercice : mettez Hello Odoo à niveau afin de réimporter le gabarit XML.
Résultat attendu : l'URL /training/notes renvoie la page uniquement après une connexion Odoo. Un envoi valide crée la note, répond par une redirection 303, puis le GET suivant affiche la liste mise à jour.
6. Comparez deux sessions sur la même URL
Gardez Alex dans une fenêtre et Sam dans une autre session de navigateur indépendante. Dans les deux cas, ouvrez exactement http://127.0.0.1:8069/training/notes.
- Dans la session d'Alex, créez Alex · page QWeb. La page rechargée affiche la note avec Alex comme propriétaire et ne montre aucune note de Sam.
- Dans la session de Sam, actualisez la même URL. La note d'Alex n'apparaît pas. Créez Sam · page QWeb.
- Après la redirection, la session de Sam affiche sa nouvelle note avec Sam comme propriétaire. Actualisez ensuite la page d'Alex : la note de Sam n'y apparaît pas.
Point de contrôle — observez les deux réponses HTML
Les deux navigateurs demandent la même route et reçoivent le même gabarit. La réponse d'Alex contient Alex · page QWeb avec Alex comme propriétaire ; celle de Sam contient Sam · page QWeb avec Sam comme propriétaire. Chaque réponse omet la note de l'autre utilisateur parce que l'ORM a appliqué la règle sur les enregistrements avant le rendu.
QWeb prépare du HTML, mais la lecture et la création passent par request.env["training.note"]. Le modèle, les droits d'accès, la règle sur les enregistrements et la table PostgreSQL sont donc les mêmes que pour les vues standard.