TQ-Slator/config/packages/security.yaml
Stephan Morand 9025c64c0b Socle complet de TQ-Slator : éditeur, API, CLI, administration
Système de gestion de traductions pensé en CMS headless : un back-office
pour ceux qui traduisent, une API pour ce qui consomme.

Architecture
- Symfony 7.4 / API Platform 4.3 / MariaDB 11.4, SPA React 19 servie en
  même origine — ce qui rend viable le cookie de session plutôt qu'un
  jeton en localStorage.
- Deux APIs séparées : Management (session ou clé) et Delivery
  (stateless, clé seule). Les fusionner ferait porter à chaque lecture de
  bundle le coût de la session.
- Stockage canonique en ICU MessageFormat, sérialisation par plateforme.
  Le format d'une plateforme ne contamine pas la base.
- Publication par releases immuables ; le déploiement est un déplacement
  de pointeur, donc le rollback aussi.
- Isolation multi-organisation par filtre Doctrine, avec un test
  d'architecture qui casse la CI si une entité échappe à l'invariant.

Éditeur, deux vues
- Par langue : source et cible, jamais douze colonnes. Grille virtualisée,
  saisie sans bouton « Enregistrer », panneau de contexte permanent.
- Par clé : une clé, toutes ses langues empilées et repliées. Répond à
  « ce libellé est-il prêt partout ? ».
- Mode Focus dans les deux : une file à vider, ⌘↵ pour enchaîner.
- Le traducteur ne voit jamais d'ICU : pastilles de variables, un champ
  par catégorie CLDR de la langue cible.

Administration
- Deux niveaux : projet (membres, clés API, plateformes) et organisation
  (annuaire des comptes, création de projets).
- Invitations par e-mail, jeton 256 bits stocké haché.
- Désactiver un compte coupe les sessions en cours, pas seulement les
  connexions suivantes.
- Les plateformes s'archivent ; ni elles ni les environnements ne se
  suppriment — la trace explique pourquoi telle clé existe.

CLI tqs
- PHAR autonome de 3 Mo, autoloader généré : le dépôt client ne dépend
  ni de Composer ni de la disponibilité de TQ-Slator.
- init / push / pull / status ; le sync est non destructif par défaut et
  son prune est scopé plateforme.

126 tests, PHPStan niveau 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:16:05 +02:00

93 lines
4.6 KiB
YAML

security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_users:
entity:
class: App\Entity\User
# Pas de `property: email` : UserRepository implémente
# UserLoaderInterface pour maîtriser la levée du filtre
# multi-organisation au chargement comme au rechargement.
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# ─── Delivery API ────────────────────────────────────────────────────
# Stateless, clé API uniquement, aucun cookie. Firewall distinct de la
# Management API : les deux n'ont ni le même mode d'authentification, ni
# la même surface, ni la même sensibilité à la latence. Les fusionner
# ferait porter à chaque lecture de bundle le coût de la session.
delivery:
pattern: ^/delivery
stateless: true
provider: app_users
entry_point: App\Security\ApiKeyAuthenticator
custom_authenticators:
- App\Security\ApiKeyAuthenticator
# ─── Management API + back-office ────────────────────────────────────
# Session cookie SameSite=Lax plutôt que JWT en localStorage : le SPA est
# servi en same-origin, le cookie HttpOnly est donc immunisé contre
# l'exfiltration par XSS, et une révocation est immédiate.
main:
lazy: true
provider: app_users
entry_point: App\Security\ApiEntryPoint
# Deux modes d'authentification cohabitent ici, et c'est nécessaire :
# le back-office ouvre une session, le CLI présente une clé API.
# ApiKeyAuthenticator::supports() ne répond que si un en-tête
# d'autorisation est présent, les deux ne se marchent donc pas dessus.
custom_authenticators:
- App\Security\ApiKeyAuthenticator
json_login:
check_path: api_auth_login
username_path: email
password_path: password
success_handler: App\Security\AuthenticationHandler
failure_handler: App\Security\AuthenticationHandler
logout:
path: api_auth_logout
login_throttling:
max_attempts: 5
interval: '15 minutes'
access_control:
# Endpoints publics : connexion et acceptation d'invitation. Un invité
# n'a par définition pas encore de compte.
- { path: ^/api/v1/auth/login$, roles: PUBLIC_ACCESS }
# Consultation ET acceptation : celui qui clique sur le lien n'a par
# définition pas encore de compte, et il doit pouvoir voir à quoi il est
# invité avant de choisir un mot de passe.
- { path: ^/api/v1/invitations/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/health$, roles: PUBLIC_ACCESS }
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
# ROLE_API_KEY et non PUBLIC_ACCESS : sans en-tête d'autorisation, le
# point d'entrée du firewall renvoie 401. Aucun endpoint de delivery ne
# peut ainsi oublier d'exiger une clé.
- { path: ^/delivery, roles: ROLE_API_KEY }
# L'administration globale est hors de portée d'une clé API, quelles que
# soient ses permissions : une clé vit dans un dépôt et une chaîne
# d'intégration continue, elle n'a rien à faire près des comptes. Les
# attributs #[IsGranted] des contrôleurs disent déjà la même chose ; cette
# ligne est la deuxième serrure, celle qui tient si un contrôleur futur
# oublie la première.
- { path: ^/api/v1/admin/, roles: ROLE_SUPER_ADMIN }
# IS_AUTHENTICATED_FULLY et non ROLE_USER : la Management API accepte deux
# identités, un utilisateur du back-office et une clé API. Exiger un rôle
# propre à l'une exclurait l'autre. Le tri fin appartient aux Voters, qui
# savent raisonner sur les deux.
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
when@test:
security:
password_hashers:
# Réduit le coût du hachage dans les tests. Ne jamais reprendre ces
# valeurs ailleurs.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4
time_cost: 3
memory_cost: 10