Aller au contenu principal

SEC-P0-001B4 — AI quota authorization hardening

Scope

  • Route corrigée : GET /api/v1/ai/quota?actor_id=...
  • Domaine : lecture du quota IA rattaché à un acteur.
  • Hors périmètre : B5 Backoffice/noCreditDebit, B6 cache/quota structurels, DB-001, unicité ai_user_quotas, tarification, Stripe, nouvelle permission spécialisée, frontend.

Divergence initiale et arbitrage

La règle générique issue des routes B1/B2/B3 pouvait laisser penser que toute route IA avec actor_id devait utiliser une permission acteur (manage_publications ou manage_actor).

L’arbitrage B4 distingue volontairement la lecture de quota :

  • manage_actor n’est pas suffisant ;
  • aucune nouvelle permission n’est créée ;
  • l’accès est réservé au propriétaire réel de l’acteur ou à l’administrateur global selon le comportement existant.

Raison : la réponse expose des données d’usage et commerciales (monthly_limit, monthly_used, bonus_credits, available, is_unlimited, reset_at) et le service sous-jacent peut lire actor_subscriptions et subscription_plans.

Mécanismes d’autorisation utilisés

Le contrôle utilise le mécanisme existant ActorAccessService::isOwner($user, $acteurId).

Ce mécanisme s’appuie sur le modèle canonique acteur_collaborateurs :

  • owner réel : rôle owner dans acteur_collaborateurs pour l’acteur demandé ;
  • admin global : Profile::isAdmin() via les rôles applicatifs admin ou platform_admin.

La projection legacy user_acteurs seule ne constitue pas une preuve d’ownership canonique pour cette route.

Ordre d’exécution sécurisé

Avant tout appel à AIQuotaService::getOrCreate() :

  1. l’utilisateur est authentifié par le middleware existant ;
  2. actor_id est validé au format UUID ;
  3. ActorAccessService::isOwner(...) vérifie owner ou admin global ;
  4. un refus retourne 403.

Ainsi, sur 403, la route ne déclenche pas :

  • création ou modification dans ai_user_quotas ;
  • lecture de plan via actor_subscriptions ou subscription_plans ;
  • écriture de cache ;
  • journalisation d’usage IA ;
  • consommation de crédit ou de boost ;
  • exposition de données quota tiers.

Consommateur actuel

Le consommateur frontend réel identifié est le Backoffice admin. Aucun changement frontend n’est requis : l’URI, la query string, la structure JSON et le calcul de quota restent inchangés pour les accès autorisés.

Tests ajoutés

Les tests AITest couvrent :

  • owner → 200 ;
  • admin global → 200 ;
  • collaborateur actif avec manage_actor403 ;
  • collaborateur avec autre permission → 403 ;
  • collaborateur inactif → 403 ;
  • utilisateur propriétaire d’un autre acteur → 403 ;
  • utilisateur sans acteur → 403 ;
  • relation legacy user_acteurs seule → 403 ;
  • régression IDOR avec actor_id falsifié → 403.

Pour les refus, les tests prouvent que AIQuotaService::getOrCreate() n’est pas appelé, qu’aucune donnée quota n’est retournée, que les quotas existants restent inchangés, qu’aucune ligne quota n’est créée quand elle n’existe pas, et que les compteurs ai_usage_logs, ai_cache et boost_usages restent inchangés.

Décision future

Si le Workspace doit afficher les crédits IA à des collaborateurs non owners, une décision produit/RBAC dédiée sera nécessaire. Cette évolution ne fait pas partie de B4 et ne doit pas réutiliser implicitement manage_actor.