Pilote Orléans · configuration, pas un fork · aucun lieu inventé

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 dangerouslySetInnerHTML sur 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_ADMIN et CITY_MANAGER avant 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).

ActionUSERSTAFFMANAGEROWNEREDITORMODCITY_MGRSUPPORTSUPER
Lire le publicouiouiouiouiouiouiouiouioui
Favori / suiviouiouiouiouiouiouiouiouioui
Demander un claim sur un lieu non déjà membreouiouiouiouiouinonnonnonnon
Live update / event / offre du lieunonouiouiouinonnonnonnonnon
Publication au nom de la ville (CITY_EDITOR, source distincte)nonnonnonnonnonnonouinonoui
Éditer texte / catégories affichéesnonnonouiouiouinonouinonoui
Éditer horaires, closed_until, prixnonnonouiouinonnonnonnonoui
Inviter staffnonnonnonouinonnonnonnonnon
Voir stats du lieunonlectureouiouinonnonagrégats villeouioui
Campagne du lieunonnonouiouinonnonnonnonoui
DNA / zones / flags villenonnonnonnonnonnonouinonoui
Rollback DNAnonnonnonnonnonnonouinonoui
Modération villenonnonnonnonnonouiouiouioui
Suspendre un lieunonnonnonnonnonouiouinonoui
Fusionner deux entitésnonnonnonnonnonnonouinonoui
Import de lotnonnonnonnonnonnonouinonoui
Exportnonnonnonson lieunonnonsa villeticketoui
SEO / articlesnonnonnonnonouinonouinonoui
Approuver claimnonnonnonnonnonouiouiouioui
Users, billing, flags globauxnonnonnonnonnonnonnonlecture usersoui
Audit lecturenonnonnonnonnonvillevilleouioui

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çoit forbidden sur 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 VALIDATED quand le code existera.
  • Hébergement UE : choix de déploiement, à confirmer contractuellement. EXTERNAL / juridique transferts.
  • Revue alcool, UGC, documents de claim : LEGAL REVIEW REQUIRED.