# Sécurité des données

Cette page dit comment protéger les lignes de votre base contre ceux qui tiennent la clé publiable : les politiques de sécurité au niveau des lignes (RLS), l'épreuve d'une politique sous l'identité d'un utilisateur, le classement des colonnes personnelles que les agents ne lisent pas, et l'analyse quotidienne de la posture de sécurité.

## Le principe

La clé publiable est dans le code de votre navigateur : n'importe qui peut la lire. Ce qui protège vos données, ce sont donc les politiques que la base applique à chaque requête, pas l'application.

- **Une table n'est servie par l'API que si elle porte la sécurité au niveau des lignes ET au moins une politique.** Les droits (`select`, `insert`, `update`, `delete` pour les rôles anonyme et authentifié) suivent d'office, et tombent dès que la table perd l'une ou l'autre (LIM-11). Supabase, lui, accorde toutes les tables de `public`.
- **Les rôles portent le nom de la base en préfixe.** Un `grant … to authenticated` ou un `create policy … to authenticated` écrit tel quel est refusé (rôle inconnu, `42704`). Écrivez la politique sans `TO` : elle vaut pour tous les rôles, et `auth.uid()` distingue l'utilisateur connecté. `auth.role()` et `auth.jwt()` rendent, eux, les noms de Supabase.
- **Écrivez `(select auth.uid())`, pas `auth.uid()` nu.** La forme en sous-requête est évaluée une fois par requête, au lieu d'une fois par ligne. La plateforme refuse la forme nue et rend sa réécriture.

## Les politiques proposées

Pour chaque table du schéma `public`, la plateforme engendre une proposition, sans modèle d'IA, d'après la structure de la table. Trois régimes, essayés dans cet ordre :

| Régime | Quand | Ce qui est proposé |
| --- | --- | --- |
| Propriétaire | Une colonne `uuid` rattache la ligne à un utilisateur : une clé étrangère vers `auth.users(id)`, sinon un nom de colonne conventionnel. | Chacun voit et modifie ses lignes. |
| Par le parent | Aucune colonne, mais une clé étrangère mène à une table qui appartient à quelqu'un (lignes de commande, tables de liaison). | La ligne appartient au propriétaire de son parent. |
| Fermée | Ni l'un ni l'autre. | La table est fermée aux utilisateurs et ouverte à la seule clé de service. |

La proposition porte les droits, quatre politiques (une par commande) pour le rôle authentifié, une politique pour la clé de service, et l'index qui sert le prédicat. Le rôle anonyme ne reçoit rien. Ouvrir une table en lecture à tous (`USING (true)`) n'est jamais proposé : c'est une décision qui vous revient.

### L'épreuve avant la validation

Avant de valider une politique, la plateforme l'éprouve sous quatre identités, avec de vrais utilisateurs de la base : un visiteur anonyme, le propriétaire d'une ligne, un autre utilisateur, et la clé de service. Une ligne lisible par qui ne la possède pas est un défaut : la politique ne s'applique alors qu'après la confirmation d'une personne, et le rapport cite la ligne.

### Depuis la console

Ouvrez **Politiques** (depuis **Données**). Les tables sont à gauche, avec leur état ; la table choisie montre au centre sa politique, sa forme et la proposition ; à droite, de quoi l'éprouver sous l'identité d'un utilisateur.

1. Choisissez une table et relisez la proposition, ou écrivez votre propre prédicat.
2. Appliquez : le rapport des quatre identités s'affiche.
3. Si une ligne est vue sans être possédée, confirmez ou corrigez.

Désactiver la sécurité au niveau des lignes d'une table est un autre geste, du rang `admin` : il se confirme en deux temps, en retapant le nom de la table, et l'ouvrier de la plateforme l'exécute (`database.rls.configure`). Réactiver ne se confirme pas. Ce geste est fermé aux agents.

### Par un agent, la ligne de commande ou l'API

| Geste | Outil MCP | Ligne de commande |
| --- | --- | --- |
| Lire les propositions | `database_policy_list` | `bastide database policy list` |
| Appliquer la proposition d'une table | `database_policy_write_proposed` | `bastide database policy write_proposed` |
| Appliquer votre prédicat | `database_policy_write` | `bastide database policy write` |
| Activer ou désactiver la RLS | aucun | `bastide database rls configure` |

Appliquer un prédicat d'appartenance (rang `maintainer`, portée `db:write`) :

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "table": "commandes",
  "policy": "proprietaire_id = (select auth.uid())",
  "verifyAs": ["anon", "authenticated", "service_role"],
  "idempotencyKey": "politique-commandes-2026-09-14"
}
```

Si l'épreuve trouve une ligne indûment lisible, l'appel est refusé sous `confirmation_requise` avec un nonce ; une personne le rejoue avec `confirmationNonce`. La réponse porte `applied`, `rewrite` (la forme optimisée, si la vôtre a été refusée), `verifications`, `report` et `indexCreated`. Une politique se remplace par un nouvel appel, qui retire la précédente avant de poser la sienne.

## Éprouver une politique sous l'identité d'un utilisateur

L'usurpation montre ce qu'un utilisateur précis de votre application voit vraiment.

1. **Ouvrir une session** pour un utilisateur de `auth.users`, désigné par son identifiant (`database_impersonation_create`). Elle dure trente minutes, sans prolongation. L'audit garde qui l'a ouverte et pour quel utilisateur.
2. **Lire** sous son identité (`database_impersonation_read`) : une instruction, sous le rôle authentifié, en transaction lecture seule, cent lignes au plus. `denied` dit si la base a refusé la lecture.
3. **Écrire** sous son identité (`database_impersonation_write`) : une écriture réelle, qui ne part qu'après la confirmation d'une personne en deux temps. Les politiques s'y appliquent comme pour l'utilisateur.

Dans la console, le panneau de droite de **Politiques** fait les mêmes gestes : choisissez l'utilisateur, ouvrez la session, puis lisez. Le bouton d'écriture ne l'exécute pas directement : il ouvre une fenêtre qui dit ce qui va se passer avant que vous ne validiez.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "impersonationId": "usu_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "sql": "select id, total from commandes order by id"
}
```

## Les colonnes personnelles

Vous pouvez classer des colonnes « donnée personnelle » : adresse, téléphone, date de naissance. Une colonne classée **ne se lit par aucun agent** : toute instruction d'un agent dont le plan la lit, l'écrit ou la passe à une fonction du projet est refusée sous `colonne_personnelle_refusee`, dans l'éditeur SQL comme sous usurpation. Une personne la lit toujours depuis la console.

- La liste vit au plan de contrôle : la base n'est pas touchée, et une colonne peut être classée avant d'exister.
- Elle se remplace entière : ce qui n'y est plus est déclassé. Cinq cents colonnes au plus.
- Seule une personne la règle (rang `maintainer`) : déclasser une colonne l'ouvrirait aux agents, et l'opération `database.personal_columns.configure` leur est donc fermée.
- Un projet créé par un agent et pas encore revendiqué ne reçoit aucune donnée personnelle (`projet_non_revendique`).

Dans la console : **Données**, section « Sécurité », carte des colonnes personnelles. Par la ligne de commande : `bastide database personal_columns configure`. Par l'API : `PUT /v1/projects/{projectId}/database/personal-columns`.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "columns": [
    { "schema": "public", "table": "clients", "column": "courriel" },
    { "schema": "public", "table": "clients", "column": "telephone" }
  ],
  "idempotencyKey": "classement-clients-2026-10-13"
}
```

## La posture de sécurité

Chaque jour, la plateforme analyse votre base et relève cinq genres de défauts, du plus grave au moins grave :

| Constat | Enjeu | Remède |
| --- | --- | --- |
| Table exposée sans politique | Quiconque tient la clé publiable lit ou écrit toutes ses lignes. | Appliquer la politique proposée. |
| Colonne personnelle lisible de la clé publiable | Une colonne classée est lue par le rôle anonyme. | Retirer le droit de lecture au rôle anonyme. |
| Clé secrète vue d'un navigateur | Son secret est dans le code d'une page. | Frapper une clé neuve côté serveur, puis révoquer celle-ci. |
| Politique toujours vraie | Elle ouvre la table aux rôles de l'API sans condition. | La remplacer par un prédicat d'appartenance. |
| Politique sans index | Chaque ligne réévalue le prédicat. | Créer l'index proposé. |

Chaque constat porte sa correction, et la posture les réunit en une migration que vous appliquez (voir [Branches et migrations](branches.md)). Ce que l'analyse n'a pas pu mesurer est dit comme tel (`notMeasured`), jamais comme une base saine.

- **Console** : **Données**, section « Sécurité » ; la colonne de droite de la section « Tables » en donne le résumé.
- **Agent** : `database_posture_read` ; `analyzedAt` nul veut dire qu'aucune analyse n'a encore eu lieu.
- **Correction en demande de fusion** : activée, chaque correction nouvelle part sur une branche `bastide/…` de votre dépôt GitHub, en demande de fusion ; rien n'est poussé sur votre branche. Désactivée par défaut, elle se règle depuis la console ou par `database.posture.configure`, fermée aux agents.

## Erreurs fréquentes

- **Une table renvoie zéro ligne par l'API** : elle n'a pas de politique, ou sa politique n'accorde rien à ce rôle. Lisez **Politiques**.
- **`42704`** : vous avez nommé `authenticated` ou `anon` ; retirez le `TO`.
- **La politique est refusée pour sa forme** : reprenez la réécriture rendue dans `rewrite`.
- **`confirmation_requise`** : l'épreuve a trouvé une ligne indûment lisible ; lisez le rapport avant de confirmer.
- **`usurpation_expiree`** : la session a passé trente minutes ; ouvrez-en une autre.
- **`colonne_personnelle_refusee`** : la colonne est classée ; une personne la lit depuis la console.

## Voir aussi

- [Authentification](auth.md), [Base de données](database.md), [Sécurité des agents](agent-safety.md), [Sécurité](security.md)
- Référence : [database.policy.write](../reference/operations/database.policy.write.md), [database.rls.configure](../reference/operations/database.rls.configure.md), [database.impersonation.create](../reference/operations/database.impersonation.create.md), [database.personal_columns.configure](../reference/operations/database.personal_columns.configure.md), [database.posture.read](../reference/operations/database.posture.read.md)
