learn.chetana.fr

La visibilité qui s'hérite (pourquoi RLS gagne)

14 min readAdvanced

Trois options, une gagnante

Pour appliquer la visibilité par utilisateur, trois familles :

OptionVerdict
A. RLS + GUC (leçon précédente)✅ couvre lecture ET écriture, et hérite partout
B. Vues de visibilité (v_visible_*)❌ ne scopent pas les écritures → policy quand même nécessaire = logique dupliquée ; et il faut réécrire toutes les requêtes pour cibler les vues (une oubliée = fuite)
C. Filtrage applicatif (WHERE user_sub = …)❌ à répliquer sur inbox + listes + détail + chaque tool + chaque export ; aucun garde-fou substrat ; c'est l'endroit où on en oublie un

Ce qui fait gagner A, ce n'est pas l'élégance — c'est l'héritage.

Ce qui hérite gratuitement

Les tables filles. Les lignes de détail, pièces jointes, messages de conversation n'ont pas de policy de visibilité propre : leur policy dit « visible si la ligne parente l'est » (EXISTS (SELECT 1 FROM parent WHERE …)). Comme cette sous-requête est elle-même filtrée par la RLS du parent, elle hérite automatiquement de la visibilité utilisateur. Zéro migration sur les filles.

Les vues. Le copilot lit des vues security_invoker = true (cf. leçon RLS). Une vue en security_invoker s'exécute avec les droits de l'appelant → elle applique la RLS des tables de base. Donc si un tool du chat ouvre sa session en session_with_operator, le chat ne peut pas voir le portefeuille d'un autre utilisateur — sans une ligne de code dédiée dans les tools.

Les exports. L'export ERP, l'export CSV tournent sous session_with_operator → filtrés par la même policy. L'exigence « un standard n'exporte que son périmètre » est satisfaite par construction, pas par vigilance.

Mettre la règle dans la RLS, c'est la mettre sous tout ce qui lit la base. Tout ce qui passe par la base — connu ou futur — hérite de la règle. C'est l'opposé du filtrage applicatif, où chaque nouveau chemin de lecture doit se souvenir de filtrer.

« La visibilité suit le compte, pas l'auteur »

Une règle métier délicate : un devis créé par Alice sur un compte ensuite affecté exclusivement à Bob → Alice perd la visibilité. Avec un filtrage applicatif, ça se code (et s'oublie). Avec la RLS d'Option A, c'est impossible à violer : la policy ne regarde jamais la colonne « créé par ». Elle ne lit que le compte et son affectation. La visibilité bascule à l'affectation, structurellement, sans code applicatif. Ce que la policy ne lit pas ne peut pas être une faille.

Deux décisions qui comptent

La clé d'identité de la policy. On compare app.user_id à quoi ? Au sub de l'identité (le claim du JWT), déjà présent dans la requête et déjà stocké dans les colonnes d'attribution → comparaison texte = texte, aucun join dans le prédicat RLS chaud. L'alternative (un id interne de membre) est plus « propre » en modélisation mais impose un join dans chaque prédicat exécuté par ligne — coût perf réel. La cohérence avec l'existant prime sur la pureté.

La migration sans friction. Comment enrôler les tenants existants sans backfill douloureux ?

  • JIT bootstrap : au 1er passage après déploiement, si le tenant n'a aucun membre, on crée le membre historique en admin/actif. L'utilisateur seul redevient admin sans rien faire.
  • Garde-fou : dès qu'un membre existe, un utilisateur sans invitation ne doit pas s'auto-promouvoir (sinon trou de sécurité) → rejet, l'invitation devient requise.
  • Rien d'affecté = tout visible (la policy tombe sur NOT EXISTS) → un tenant mono-utilisateur ne voit aucune différence. La feature est invisible tant qu'on ne s'en sert pas.
🧩 Quiz1/4

Pourquoi les vues de visibilité (Option B) ne suffisent-elles pas ?

🃏 Flashcards1/5