Livrable 18
Sécurité et RBAC
19 — Matrice RBAC
Baseline
OWASP, adapté à une app web avec données publiques et rôles :
- Cookies de session : httpOnly, Secure, SameSite=Lax (Strict si aucun flux cross-site requis).
- CSRF : mutations cookie-authenticated avec jeton ou SameSite + vérification d'origine. Ne pas s'en remettre au « SameSite suffit » pour les admin.
- CSP stricte. Exception connue de cet environnement : le runtime injecte un script de plateforme
https://grok.com; la CSP ne doit pas le bloquer ici. En production hors plateforme, CSP sans ce besoin. - HSTS sur le domaine de prod.
- Requêtes paramétrées uniquement (pas de SQL concaténé).
- XSS : pas de
dangerouslySetInnerHTMLsur le contenu pro/CMS sans sanitation. Le markdown CMS est sanitizé. - Rate limit : login, recherche, signalement, publish, intent IA.
- Brute-force : délai / verrouillage progressif sur login. Pas de message qui révèle si l'e-mail existe.
- Rotation de session à la connexion et au changement de rôle.
- 2FA : obligatoire pour
SUPER_ADMINetCITY_MANAGERavant une prod réelle. Optionnelle pour les pros. TOTP, pas de SMS par défaut (SIM swap +THIRD-PARTY). - Secrets : variables d'environnement injectées, jamais committées, jamais de clé Stripe ou LLM dans le client. Pas de défaut silencieux pour un secret.
- Dépendances : audit dans la CI quand le code existera.
- Journaux : pas de corps de requête contenant un mot de passe, un token, un document d'identité.
- Uploads : voir ci-dessous.
- Autorisation testée par rôle et par
city_id/venue_id.
Menaces spécifiques : claim frauduleux, faux événement, scraping de nos pages (rate limit + conditions), pollution SEO, injection dans le champ recherche vers le LLM, élévation city manager → autre ville, IDOR sur /pro/events/:id.
Uploads
Autorisés : images (jpeg, png, webp) et plus tard vidéo courte. Vérifier MIME et signature, taille max (politique, pas un chiffre magique non documenté — défaut proposé 8 Mo image), dimensions max, ré-encodage, strip EXIF (géoloc photo). Stockage objet UE. URL signée en lecture si non public ; médias publiés via CDN. THIRD-PARTY : aucun bucket n'est provisionné dans cette phase.
Chaque média : owner, source, license, uploaded_by. Sans licence : non public.
RBAC
Rôles :
USER · BUSINESS_OWNER · BUSINESS_MANAGER · BUSINESS_STAFF · EDITOR · MODERATOR · CITY_MANAGER · SUPPORT · SUPER_ADMIN
Un user peut avoir un rôle plateforme (USER, SUPPORT, SUPER_ADMIN) et des memberships scopés (venue_members, city_members).
| Action | USER | STAFF | MANAGER | OWNER | EDITOR | MOD | CITY_MGR | SUPPORT | SUPER |
|---|---|---|---|---|---|---|---|---|---|
| Lire le public | oui | oui | oui | oui | oui | oui | oui | oui | oui |
| Favori / suivi | oui | oui | oui | oui | oui | oui | oui | oui | oui |
| Demander un claim sur un lieu non déjà membre | oui | oui | oui | oui | oui | non | non | non | non |
| Live update / event / offre du lieu | non | oui | oui | oui | non | non | non | non | non |
Publication au nom de la ville (CITY_EDITOR, source distincte) | non | non | non | non | non | non | oui | non | oui |
| Éditer texte / catégories affichées | non | non | oui | oui | oui | non | oui | non | oui |
Éditer horaires, closed_until, prix | non | non | oui | oui | non | non | non | non | oui |
| Inviter staff | non | non | non | oui | non | non | non | non | non |
| Voir stats du lieu | non | lecture | oui | oui | non | non | agrégats ville | oui | oui |
| Campagne du lieu | non | non | oui | oui | non | non | non | non | oui |
| DNA / zones / flags ville | non | non | non | non | non | non | oui | non | oui |
| Rollback DNA | non | non | non | non | non | non | oui | non | oui |
| Modération ville | non | non | non | non | non | oui | oui | oui | oui |
| Suspendre un lieu | non | non | non | non | non | oui | oui | non | oui |
| Fusionner deux entités | non | non | non | non | non | non | oui | non | oui |
| Import de lot | non | non | non | non | non | non | oui | non | oui |
| Export | non | non | non | son lieu | non | non | sa ville | ticket | oui |
| SEO / articles | non | non | non | non | oui | non | oui | non | oui |
| Approuver claim | non | non | non | non | non | oui | oui | oui | oui |
| Users, billing, flags globaux | non | non | non | non | non | non | non | lecture users | oui |
| Audit lecture | non | non | non | non | non | ville | ville | oui | oui |
CITY_EDITOR n'est pas un rôle. C'est une action auditée : la source écrite est CITY_EDITOR, jamais BUSINESS_VERIFIED. Un super admin qui publie « comme le bar » utilise la même action, pas le compte du pro.
Un owner peut revendiquer un autre lieu. Il ne revendique pas un lieu dont il est déjà membre. GROUP n'est pas un rôle : c'est un code de plan et un billing_account.kind.
EDITOR ne change pas les horaires. La note « édition éditoriale » s'arrête au texte, aux liens et au rail de sélection.
POST /reports est public et rate-limité. Aucun badge public avant une résolution qui le demande explicitement.
Refus par défaut. Toute case « oui » est un test d'intégration prévu.
SUPPORT ne change pas la DNA et ne lit pas les documents d'identité en clair au-delà du besoin de ticket. Accès document = audit + rôle city manager ou super.
Audit
Actions : publish DNA, override flag, approve/reject claim, merge entités, suspend venue, change role, export, delete user, moderation resolve, price plan change, campaign status.
Champs : actor_id, action, entity, before, after, at. Immuable (insert only). Pas d'update.
Décisions
D1 — RBAC en base, vérifié serveur
- Alternative rejetée : rôles uniquement dans le JWT sans relecture (stale privilege).
- Compromis : une requête de membership par mutation. Acceptable.
- Multi-ville :
city_members.city_id. Test : un manager d'Orléans reçoitforbiddensur Nantes.
D2 — Pas de sécurité par obscurité des URL
UUID non devinable aide ; ce n'est pas l'autorisation.
Flags
- 2FA et pentest : pas faits.
IMPLEMENTED — NOT YET VALIDATEDquand le code existera. - Hébergement UE : choix de déploiement, à confirmer contractuellement.
EXTERNAL/ juridique transferts. - Revue alcool, UGC, documents de claim :
LEGAL REVIEW REQUIRED.