Skip to content

Commit 35f8229

Browse files
committed
feat: postgres, add Row-Level Security (RLS)
1 parent 89449ac commit 35f8229

6 files changed

Lines changed: 910 additions & 87 deletions

File tree

sandbox/postgres/README.md

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ relationnelle. Conçu pour une cible de **500 000 utilisateurs actifs**.
1717

1818
## Philosophie
1919

20-
Ce projet applique le paradigme **SoA** (_Structure of Arrays_), inspiré de l'ECS
20+
Ce projet applique le paradigme **SoA** (*Structure of Arrays*), inspiré de l'ECS
2121
des moteurs de jeu et du DOD bas niveau :
2222

2323
- **Entity** — un identifiant entier pur, sans donnée métier.
@@ -41,34 +41,36 @@ l'interface [schema.org](https://schema.org) par-dessus les composants fragment
4141
.
4242
├── master_schema_ddl.pgsql # Blueprint immuable — DDL pur
4343
├── master_schema_dml.pgsql # Seed data — dev / CI uniquement
44-
├── architecture_decision_records.md # 25 arbitrages architecturaux
44+
├── architecture_decision_records.md # 28 arbitrages architecturaux
4545
├── README.md
4646
└── tests/
4747
├── 01_schema_and_security.sql # Types physiques, BRIN, RBAC, SECURITY DEFINER
4848
├── 02_identity_logic.sql # Comptes, slugs, bitmask, connexions
4949
├── 03_content_logic.sql # Documents, révisions, commentaires ltree
5050
├── 04_commerce_logic.sql # Stock, snapshots de prix, agrégats
51-
└── 05_tag_hierarchy.sql # Mots clefs hiérarchisés
51+
├── 05_tag_hierarchy.sql # Closure Table, profondeur, breadcrumb
52+
└── 06_rls_policies.sql # RLS GUC stateless, SELECT/UPDATE isolation
5253
```
5354

5455
### `master_schema_ddl.pgsql`
5556

5657
Le schéma complet en un fichier autonome. Contient dans l'ordre d'exécution :
5758

58-
| Section | Contenu |
59-
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
60-
| 0 — Initialisation | Création de l'utilisateur, de la base, connexion |
61-
| 1 — Extensions | `unaccent`, `ltree`, `pg_trgm`, `postgis` |
62-
| 2 — Schémas | `identity`, `geo`, `org`, `commerce`, `content` |
63-
| 3 — Spines | `identity.entity`, `org.entity`, `content.document` |
64-
| 4 — Fondation | `geo.place_core/content`, `identity.permission_bit`, `identity.role` |
65-
| 5–8 — Composants | Toutes les tables physiques par domaine |
66-
| 9 — Fonctions | `fn_update_modified_at`, `fn_slug_deduplicate`, `fn_revision_num`, `has_permission` |
67-
| 10 — Triggers | Triggers `modified_at`, déduplication de slug, numérotation des révisions |
68-
| 11 — Procédures | `create_account`, `create_document`, `create_comment`, `create_transaction`, `create_transaction_item`, etc. |
69-
| 12 — Vues | Toutes les vues sémantiques schema.org |
70-
| 13 — Permissions | `GRANT SELECT + EXECUTE` sur `marius_user` · calibrage autovacuum |
71-
| 14 — Verrouillage ECS | `marius_admin` · révocation DML globale · `SECURITY DEFINER` (ADR-020) |
59+
| Section | Contenu |
60+
|---|---|
61+
| 0 — Initialisation | Création de l'utilisateur, de la base, connexion |
62+
| 1 — Extensions | `unaccent`, `ltree`, `pg_trgm`, `postgis` |
63+
| 2 — Schémas | `identity`, `geo`, `org`, `commerce`, `content` |
64+
| 3 — Spines | `identity.entity`, `org.entity`, `content.document` |
65+
| 4 — Fondation | `geo.place_core/content`, `identity.permission_bit`, `identity.role` |
66+
| 5–8 — Composants | Toutes les tables physiques par domaine |
67+
| 9 — Fonctions | `fn_update_modified_at`, `fn_slug_deduplicate`, `fn_revision_num`, `has_permission` |
68+
| 10 — Triggers | Triggers `modified_at`, déduplication de slug, numérotation des révisions |
69+
| 11 — Procédures | `create_account`, `create_document`, `create_comment`, `create_transaction`, `create_transaction_item`, etc. |
70+
| 12 — Vues | Toutes les vues sémantiques schema.org |
71+
| 13 — Permissions | `GRANT SELECT + EXECUTE` sur `marius_user` · calibrage autovacuum |
72+
| 14 — Verrouillage ECS | `marius_admin` · révocation DML globale · `SECURITY DEFINER` · `BYPASSRLS` (ADR-020/028) |
73+
| 15 — RLS | Politiques stateless GUC sur `content.core`, `transaction_core`, `account_core` (ADR-028) |
7274

7375
### `master_schema_dml.pgsql`
7476

@@ -82,7 +84,7 @@ d'écriture que la production.
8284

8385
### `architecture_decision_records.md`
8486

85-
25 arbitrages architecturaux, ordonnés par importance décroissante. Chaque
87+
28 arbitrages architecturaux, ordonnés par importance décroissante. Chaque
8688
entrée documente ce qui **n'est pas déductible de la lecture du code** : le
8789
raisonnement derrière la décision, les alternatives écartées et leurs coûts.
8890

@@ -98,32 +100,36 @@ psql -U postgres -d marius -f tests/01_schema_and_security.sql
98100
psql -U postgres -d marius -f tests/02_identity_logic.sql
99101
psql -U postgres -d marius -f tests/03_content_logic.sql
100102
psql -U postgres -d marius -f tests/04_commerce_logic.sql
103+
psql -U postgres -d marius -f tests/05_tag_hierarchy.sql
104+
psql -U postgres -d marius -f tests/06_rls_policies.sql
101105
```
102106

103-
| Fichier | Périmètre | Tests |
104-
| ---------------------------- | ---------------------------------------------------------------------- | ----- |
105-
| `01_schema_and_security.sql` | Types INT8, absence de bpchar, BRIN, SECURITY DEFINER, RBAC, triggers | 35 |
106-
| `02_identity_logic.sql` | `create_account`, slugs, bitmask `has_permission`, `record_login` | 9 |
107-
| `03_content_logic.sql` | `create_document`, snapshot complet (ADR-021), ltree, `create_comment` | 17 |
108-
| `04_commerce_logic.sql` | Stock, snapshot de prix, sur-vente, agrégats `v_transaction` | 6 |
107+
| Fichier | Périmètre | Tests |
108+
|---|---|---|
109+
| `01_schema_and_security.sql` | Types INT8, absence de bpchar, BRIN, SECURITY DEFINER, RBAC, triggers | 42 |
110+
| `02_identity_logic.sql` | `create_account`, slugs, bitmask `has_permission`, `record_login` | 12 |
111+
| `03_content_logic.sql` | `create_document`, snapshot complet (ADR-021), ltree, `create_comment` | 17 |
112+
| `04_commerce_logic.sql` | Stock, snapshot de prix, sur-vente, agrégats `v_transaction` | 6 |
113+
| `05_tag_hierarchy.sql` | Closure Table, create_tag, sous-arbre, profondeur max, breadcrumb | 10 |
114+
| `06_rls_policies.sql` | RLS stateless GUC, SELECT/UPDATE sur content, commerce, identity | 15 |
109115

110116
---
111117

112118
## Prérequis
113119

114-
| Composant | Version minimale |
115-
| ---------- | ---------------------------------- |
120+
| Composant | Version minimale |
121+
|---|---|
116122
| PostgreSQL | **18** (async I/O, EXPLAIN MEMORY) |
117-
| PostGIS | 3.x |
123+
| PostGIS | 3.x |
118124

119125
### Extensions PostgreSQL requises
120126

121-
| Extension | Usage |
122-
| ---------- | ------------------------------------------------------------------------------ |
123-
| `unaccent` | Normalisation des accents pour les index de recherche texte |
124-
| `ltree` | Chemins matérialisés pour la taxonomie des tags et les threads de commentaires |
125-
| `pg_trgm` | Index trigrammes pour la recherche partielle sur les noms et titres |
126-
| `postgis` | Type `geometry(Point, 4326)`, index GiST géospatiaux, opérateur KNN `<->` |
127+
| Extension | Usage |
128+
|---|---|
129+
| `unaccent` | Normalisation des accents pour les index de recherche texte |
130+
| `ltree` | Chemins matérialisés pour la taxonomie des tags et les threads de commentaires |
131+
| `pg_trgm` | Index trigrammes pour la recherche partielle sur les noms et titres |
132+
| `postgis` | Type `geometry(Point, 4326)`, index GiST géospatiaux, opérateur KNN `<->` |
127133

128134
Vérification de disponibilité sur le serveur cible :
129135

@@ -253,10 +259,10 @@ SELECT * FROM identity.v_role WHERE id = 1;
253259

254260
### Vues de listing vs page complète (isolation TOAST, ADR-016)
255261

256-
| Vue | Usage | Charge TOAST |
257-
| ------------------------ | -------------------------- | ------------------- |
258-
| `content.v_article_list` | Listings, flux, navigation | Zéro |
259-
| `content.v_article` | Page article complète | Oui (`articleBody`) |
262+
| Vue | Usage | Charge TOAST |
263+
|---|---|---|
264+
| `content.v_article_list` | Listings, flux, navigation | Zéro |
265+
| `content.v_article` | Page article complète | Oui (`articleBody`) |
260266

261267
Toujours utiliser `v_article_list` pour les listings. Ne projeter `articleBody`
262268
que sur les lectures de page complète.
@@ -268,24 +274,24 @@ tables physiques. Toute mutation passe par les procédures stockées, déclarée
268274
`SECURITY DEFINER` : elles s'exécutent avec les droits du propriétaire (`postgres`)
269275
indépendamment des droits de l'appelant.
270276

271-
| Procédure | Usage |
272-
| ----------------------------------------------- | ------------------------------------------------------------------------ |
273-
| `identity.create_account(...)` | Créer un compte (entity + auth + account_core) |
274-
| `identity.create_person(...)` | Créer un profil public |
275-
| `identity.record_login(entity_id)` | Enregistrer une connexion (hot path) |
276-
| `content.create_document(...)` | Créer un article/page |
277-
| `content.publish_document(document_id)` | Publier un brouillon |
277+
| Procédure | Usage |
278+
|---|---|
279+
| `identity.create_account(...)` | Créer un compte (entity + auth + account_core) |
280+
| `identity.create_person(...)` | Créer un profil public |
281+
| `identity.record_login(entity_id)` | Enregistrer une connexion (hot path) |
282+
| `content.create_document(...)` | Créer un article/page |
283+
| `content.publish_document(document_id)` | Publier un brouillon |
278284
| `content.save_revision(document_id, author_id)` | Snapshot éditorial complet (name, slug, alt_headline, description, body) |
279-
| `content.create_comment(...)` | Insérer un commentaire (zéro dead tuple, ADR-012) |
280-
| `commerce.create_transaction_item(...)` | Ligne de commande avec snapshot du prix en centimes (INT8, ADR-014) |
285+
| `content.create_comment(...)` | Insérer un commentaire (zéro dead tuple, ADR-012) |
286+
| `commerce.create_transaction_item(...)` | Ligne de commande avec snapshot du prix en centimes (INT8, ADR-014) |
281287

282288
### Rôles PostgreSQL
283289

284-
| Rôle | Droits | Usage |
285-
| -------------- | --------------------------------------------- | -------------------------------- |
286-
| `marius_user` | `SELECT` + `EXECUTE` | Runtime applicatif |
290+
| Rôle | Droits | Usage |
291+
|---|---|---|
292+
| `marius_user` | `SELECT` + `EXECUTE` | Runtime applicatif |
287293
| `marius_admin` | `SELECT` + `EXECUTE` + `INSERT/UPDATE/DELETE` | Maintenance, migrations, CI seed |
288-
| `postgres` | Superutilisateur | Déploiement DDL, installation |
294+
| `postgres` | Superutilisateur | Déploiement DDL, installation |
289295

290296
`marius_admin` hérite de `marius_user` via `GRANT ... WITH INHERIT TRUE`.
291297
En environnement hautement sécurisé, désactiver le `LOGIN` direct sur
@@ -295,8 +301,8 @@ En environnement hautement sécurisé, désactiver le `LOGIN` direct sur
295301

296302
## Cas d'usage : de la frugalité à l'extreme scale
297303

298-
L'architecture Marius n'est pas dimensionnée _pour_ 500 000 utilisateurs —
299-
elle est dimensionnée _par_ les contraintes physiques de PostgreSQL. Cette
304+
L'architecture Marius n'est pas dimensionnée *pour* 500 000 utilisateurs —
305+
elle est dimensionnée *par* les contraintes physiques de PostgreSQL. Cette
300306
rigueur mécanique la rend pertinente à n'importe quelle échelle.
301307

302308
---
@@ -327,7 +333,7 @@ gonfleront jamais les tables hot path, quelle que soit leur longueur.
327333
massifs, catalogues produits dynamiques.
328334

329335
À ce volume, le coût dominant n'est plus le I/O disque mais la **fragmentation
330-
progressive du stockage** (_bloat_) et la pression sur l'autovacuum.
336+
progressive du stockage** (*bloat*) et la pression sur l'autovacuum.
331337

332338
**Zéro dead tuple structurel** (ADR-012) : `content.create_comment()` effectue
333339
une seule écriture heap par commentaire. Sur 10 000 commentaires/jour, l'absence
@@ -392,12 +398,12 @@ ORDER BY n_dead_tup DESC
392398
LIMIT 20;
393399
```
394400

395-
| Table | Source de dead tuples | fillfactor |
396-
| ----------------------- | ----------------------------------------- | ---------- |
397-
| `identity.auth` | `last_login_at` à chaque connexion | 70 |
398-
| `commerce.product_core` | `stock` à chaque vente | 80 |
399-
| `content.core` | `status` à chaque changement de cycle | 75 |
400-
| `content.comment` | Suppressions de modération (`status = 9`) | défaut |
401+
| Table | Source de dead tuples | fillfactor |
402+
|---|---|---|
403+
| `identity.auth` | `last_login_at` à chaque connexion | 70 |
404+
| `commerce.product_core` | `stock` à chaque vente | 80 |
405+
| `content.core` | `status` à chaque changement de cycle | 75 |
406+
| `content.comment` | Suppressions de modération (`status = 9`) | défaut |
401407

402408
### Vérification de l'isolation TOAST
403409

@@ -455,4 +461,4 @@ ORDER BY usename, state;
455461

456462
---
457463

458-
_Architecture ECS/DOD · PostgreSQL 18 · Projet Marius_
464+
*Architecture ECS/DOD · PostgreSQL 18 · Projet Marius*

0 commit comments

Comments
 (0)