# Système de gestion des droits (IAM / Policy)

Ce module gère l'attribution et la vérification des permissions dans le Reef.
Il s'appuie sur un service IAM externe (via l'API Jane générée) et suit le modèle **PBAC Resource-Based**.

---

## Qu'est-ce que le PBAC Resource-Based ?

**PBAC** (Policy-Based Access Control) signifie que les règles d'accès sont définies par des *policies* — des objets métier qui calculent dynamiquement les permissions à accorder. Contrairement au RBAC (Role-Based Access Control), une policy peut tenir compte du contexte de l'entité (propriétaire, état, équipe…).

**Resource-Based** signifie que les permissions sont toujours attachées à une *ressource* ou à un *groupe de ressources* IAM précis, jamais à un sujet abstrait. Chaque permission accordée dit : *"ce principal peut faire telle action sur cette ressource"*.

## Isolation

Par défaut, l'IAM isole chaque Client (application). Par exemple, le BSM défini le type de resource reef:bsmApi:veterinaryEstablishment,
ainsi que des permissions qui lui sont propres dessus (par exemple : "readPublic"). Les autres projets ne peuvent pas acccorder
la permission readPublic via des policies. De plus, lors d'une requête pour déterminer les permissions sur une resource ou un type de resources,
les permissions sur BSM n'apparaîtront **QUE** pour le BSM, un autre projet ne verra pas ces permissions.

Il existe tout de même un mécanisme permettant de contourner ce système pour des cas très spécifiques. Il reste lourd à
mettre en place et ne doit être utilisé qu'exceptionnelement.

### Flux complet

```
Entité Doctrine/API         Policy                    IAM                    Application
 (Pet, Establishment…)       (calcule les droits)      (stocke les droits)    (vérifie les droits)

      │                          │                          │                        │
      │──── apply(entity) ──────▶│                          │                        │
      │                          │── PolicyResult ─────────▶│                        │
      │                          │   subject: Resource      │ stocke les grants      │
      │                          │   grants: [principal     │                        │
      │                          │            → permissions]│                        │
      │                          │                          │                        │
      │                          │                          │◀── check(resource, ────│
      │                          │                          │     bearerToken)       │
      │                          │                          │                        │
      │                          │                          │──── [permissions] ────▶│
```

---

## Architecture (Hexagonale)

```
lib/
├── Domain/Security/          ← Cœur métier, zéro dépendance externe
│   ├── Model/                ← Value Objects : Resource, ResourceType, ResourceGroup
│   ├── Policy/               ← PolicyInterface<T>, PolicyResult, PolicyResultGrant
│   ├── Principal/            ← PrincipalInterface, Principal
│   ├── Port/                 ← Interfaces sortantes : PermissionCheckerInterface, PolicyResultPublisherInterface…
│   └── Exception/            ← IAMException
│
├── Application/Security/     ← Use Cases orchestrant le Domain
│   ├── UseCase/              ← SyncPoliciesUseCase, BatchSyncPoliciesUseCase, ExecutePoliciesUseCase
│   └── Service/              ← PolicyOrchestrator, ResourceFinderService
│
└── Infrastructure/Security/  ← Adaptateurs concrets
    ├── Api/                  ← Appels à l'IAM via Jane (Publisher, Store, PermissionChecker)
    ├── Doctrine/             ← AbstractDoctrineResourceFinder
    ├── IAM/                  ← Generator, Mapper, Transformer
    ├── Symfony/              ← IAMVoter (Symfony Security)
    └── Async/                ← SymfonyMessengerPolicyPublisher
```

La règle de dépendance est stricte : **Domain ne dépend de rien**, Application dépend du Domain, Infrastructure dépend du Domain et de l'Application.

---

## Concepts clés

### Ressources IAM

Toutes les entités exposées dans l'IAM sont représentées par des **value objects immuables** à base de slugs hiérarchiques.

| Classe | Description | Exemple de slug |
|---|---|---|
| `ResourceType` | Type de ressource (catégorie) | `my:app:pet` |
| `Resource` | Instance précise d'une ressource | `my:app:pet:3` |
| `ResourceGroup` | Groupe de ressources (ex: wildcard `*`) | `my:app:pets:*` |

```php
use NoahVet\Reef\Domain\Security\Model\{Resource, ResourceGroup, ResourceType};

// Type
$type = ResourceType::fromString('my:app:pet');

// Instance : my:app:pet:3
$resource = Resource::fromString('my:app:pet', '3');
$resource = Resource::fromString($type, '3'); // équivalent

// Groupe : my:app:pets:* (toutes les instances)
// Note : ResourceGroup ajoute automatiquement le 's' sur le type slug
$group = ResourceGroup::fromString('my:app:pet', '*');
// → slug : my:app:pets:*
```

> `ResourceGroup` est utilisé pour accorder des permissions sur **toutes** les ressources d'un type d'un seul coup (exemple : un super-admin qui accède à tous les établissements).

### Principal

Un principal est un utilisateur du système dans l'IAM. Il peut-être une personne ou un compte de service. Format standard :

```
reef:iam:principal:{uuid}       ← utilisateur individuel
reef:iam:principals:{slug}      ← groupe de principals (ex: superAdmins)
```

### Etag

L'etag est un hash représentant l'état courant des données d'un sujet. Avant de publier une `PolicyResult` vers l'IAM, le système compare l'etag calculé avec celui stocké : si identiques, l'envoi est évité. **Incluez toujours toutes les données qui influencent les grants dans le calcul de l'etag.**

### Nom de la policy

Le nom retourné par `getName()` est l'identifiant de la policy dans l'IAM. Il sert de clé de partitionnement lors du stockage et de la récupération des etags (`findEtag(policyName, subject)`).

**Convention** : kebab-case, descriptif du rôle accordé (ex: `pet-owner`, `prescription-reader`, `establishment-admin`).

**Unicité** : le nom doit être unique **au sein du projet**. Deux services différents peuvent utiliser le même nom sans collision, l'IAM les isole par service.

> <font color='red'>**Attention**</font> : renommer une policy (modifier `getName()`) laisse les anciens résultats orphelins dans l'IAM — aucun nettoyage automatique n'est effectué. La policy repart de zéro et re-publie tous les grants au prochain cycle de synchronisation. <font color='red'>A EVITER ABSOLUMENT</font>

---

## Implémentation d'une Policy

L'implémentation d'une policy se passe en plusieurs étapes qui vont dépendre de votre cas spécifique.
Suivant votre situation, les étapes 1, 2 et 3 peuvent être facultatives. L'étape 3 est l'implémentation en elle même
de la policy. Pour chaque étape vous trouverez les conditions dans lesquelles vous devez l'implémenter ou non et une
procédure d'implémentation indiquant tout ce qu'il y a à faire, ainsi qu'un moyen de vérifier que votre implémentation
fonctionne.


### Étape 1 : Nouveau type de resources

> Objectif : Avoir des resources pertinentes pour porter vos permissions

**Ce cas doit rester rare**, l'ajout de nouveaux types de resources dans l'IAM n'est pas à prendre à la légère.
Avant d'envisager cette option, vérifiez que vous ne pouvez pas vous baser sur un type de resource existant.
Votre nouveau type de resource ne doit pas chevaucher un type de resource existant au risque de créer des incohérences
dans le comportement sécuritaire.

> En cas de doute, consultez votre CTO pour valider l'ajout d'un nouveau type de resources

Si malgré l'avertissement ci-dessus, vous devez créer un nouveau type de resource (ex: `Pet`) et souhaitez l'exposer dans l'IAM :

#### Mise à jour service_definition.json
Commencez par mettre à jour le fichier `config/reef/service_definition.json` :

```json
{
  "id": "my:app",
  "name": "Pet Store",
  "version": "0.0.1",
  "resources": {
    "pet": {
      "name": "Pet",
      "description": "Un animal de compagnie",
      "groupSlug": "pets",
      "permissions": {
        "takeHome": {
          "name": "Ramener au domicile",
          "description": "Autoriser la personne à ramener cet animal à la maison"
        }
      }
    }
  },
  "externalResources": {}
}
```

Le type de ressource IAM sera `my:app:pet`, la permission `my:app:pet:perm:takeHome`.

#### Generator

Le Generator transforme une entité Doctrine vers un objet `Resource` IAM.

```php
// src/IAM/Generator/PetGenerator.php
use NoahVet\Reef\Infrastructure\Security\IAM\Generator\InternalResourceGeneratorInterface;
use NoahVet\Reef\Domain\Security\Model\Resource;

class PetGenerator implements InternalResourceGeneratorInterface
{
    public function canGenerate(mixed $entity): bool
    {
        return $entity instanceof Pet;
    }

    public function generateResource(mixed $entity): Resource
    {
        return Resource::fromString('my:app:pet', (string) $entity->getId());
    }
}
```

Un `Pet` avec id `3` obtiendra le slug IAM `my:app:pet:3`.


#### Vérification de l'implémentation

Vous venez d'ajouter votre type de resource IAM, il est temps de vérifier que tout fonctionne. Vous pouvez le faire
directement depuis votre espace de dev dans votre projet Symfony :

```bash
# Enregistrement du nouveau type de resource auprès de l'IAM
make update-service-definition

# Résultat attendu : [OK] Service definition updated

make php # Entrée dans le container PHP de votre projet
php bin/console reef:resources:sync -vv # Synchronisation des ressources IAM

# Résultat attendu : Une liste de slugs IAM de resources pour chaque entity de votre projet
# Exemple :
#  - reef:bsmApi:legalStructure:1edac69a-cab7-635c-83ca-cfbc96a8c187
#  - reef:bsmApi:legalStructure:1edac6bf-eb3d-6ee4-b60f-07cbacd6a0a1
#  - reef:bsmApi:veterinaryEstablishment:1edb1c88-b298-6962-9fb3-697e06acaada
#  - reef:bsmApi:veterinaryEstablishment:1edb1c89-d6d5-6ff4-bde2-4ddad434169c
# ...
```


### Étape 2 : Déclaration de permissions sur des resources externes

> Objectif : Indiquer à l'IAM que vous souhaitez ajouter vos propres permissions sur des resources déclarées par un
> service externe. Par exemple : je veux ajouter des permissions sur les établissements vétérinaires du BSM.

Dans le cas où les permissions que vous souhaitez ajouter seront portées par des resources existantes d'autres services,
vous devez mettre à jour la section `externalResources` de votre fichier `service_definition.json` :

```json
{
  "id": "my:app",
  "name": "Pet Store",
  "version": "0.0.1",
  "resources": {},
  "externalResources": {
    "reef:bsmApi:veterinaryEstablishment": {
      "permissions": {
        "my:app:veterinaryEstablishment:perm:readPrescriptions": {
          "name": "Accès aux ordonnances d'une clinique vétérinaire",
          "description": "Seul le personnel habilité peut lire les ordonnances de sa clinique de référence"
        }
      }
    }
  }
}
```


### Étape 3 : Mise en place du Finder

> Objectif : Lister les resources qui vont porter vos permissions (qu'elles soient dans votre projet ou dans un autre projet)

Le Finder énumère toutes les entités d'un type et retrouve une entité depuis son slug IAM. Une implémentation Doctrine est fournie.

**Avant de vous lancer dans l'implémentation d'un Finder**, l'api-client vous fourni quelques Finder qui seront auto-injectés
dans des cas courants (policies concernant : VeterinaryEstablishment, LegalStructure, Manufacturer, Publisher). Pour ces
cas, vous pouvez utiliser des classes abstraites dédiées de policies dont le Finder sera directement injecté dans votre
projet Symfony.

Exemple d'implémentation d'un Finder pour des resources *locales* (gérées par votre projet) :

```php
// src/IAM/Finder/PetFinder.php
use NoahVet\Reef\Infrastructure\Security\Doctrine\AbstractDoctrineResourceFinder;

class PetFinder extends AbstractDoctrineResourceFinder
{
    public function __construct(EntityManagerInterface $em)
    {
        parent::__construct(Pet::class, 'my:app:pet:', $em);
    }
}
```

Le `$iamPrefix` est le slug de type suivi de `:`. Le Finder expose automatiquement `enumerate()` (itère toutes les entités) et `findBySlug(string)` (retrouve une entité par slug IAM).

Exemple d'implémentation d'un Finder pour des resources *distantes* (gérées par un autre projet) :

```php
use NoahVet\Reef\Business\Enumerator\EstablishmentWithTeamEnumerator;
use NoahVet\Reef\Domain\Security\Model\Resource;

class VeterinaryEstablishmentFinder implements ResourceFinderEnumeratorInterface, ReverseResourceFinderInterface
{
    public const BATCH_SIZE = 50;
    private const IAM_PREFIX = 'reef:bsmApi:veterinaryEstablishment';

    public function __construct(
        private readonly EstablishmentWithTeamEnumerator $establishmentEnumerator,
    ) {}

    public function enumerate(): \Generator
    {
        return $this->establishmentEnumerator->enumerateAll(self::BATCH_SIZE);
    }

    public function findBySlug(string $slug): mixed
    {
        $resource = Resource::fromSlug($slug);
    
        if (self::IAM_PREFIX !== (string) $resource->getResourceType()) {
            return null;
        }

        $establishments = \iterator_to_array(
            $this->establishmentEnumerator->enumerateAll(
                self::BATCH_SIZE,
                ['id' => [$resource->getResourceId()]]
            ),
            false,
        );

        return $establishments[0] ?? null;
    }
}
```

#### Vérification de l'implémentation

```bash
# Entrée dans le container PHP de votre projet
make php

# Liste des finders
php bin/console debug:container --tag reef.iam.finder

# Résultat attendu : Vous devez voir votre classe de Finder apparaître dans la liste


php bin/console reef:policies:sync -vv # Synchronisation des policies

# Résultat attendu : Une liste de resources (avec leur classe, et éventuellement leur UUID si disponible)
# Exemple :
#  Resource : App\Entity\Business\LegalStructure#1edac6bf-eb3d-6ee4-b60f-07cbacd6a0a1
#  Resource : App\Entity\Business\LegalStructure#1edac6c0-4089-64a2-9817-65880c89a104
#  ...
```


### Étape 3 : Création de la policy

> Objectif : Implémenter votre politique de sécurité avec ses règles spécifiques d'attribution de permissions

La Policy définit **qui** peut faire **quoi** et est appelée pour chaque resource d'un type défini.
Par exemple, une policy sur les établissements vétérinaires sera appelée pour chaque établissement vétérinaire.

```php
// src/IAM/Policy/PetPolicy.php
use NoahVet\Reef\Domain\Security\Policy\PolicyInterface;
use NoahVet\Reef\Domain\Security\Policy\PolicyResult;
use NoahVet\Reef\Domain\Security\Policy\PolicyResultGrant;
use NoahVet\Reef\Domain\Security\Model\Resource;

/** @implements PolicyInterface<Pet> */
class PetPolicy implements PolicyInterface
{
    public function getName(): string
    {
        return 'pet-policy';
    }

    public function canHandle(object $subject): bool
    {
        return $subject instanceof Pet;
    }

    public function subjectToString(mixed $subject): string
    {
        return (string) $subject->getId();
    }

    public function computeEtag(mixed $subject): string
    {
        // Incluez tout ce qui influe sur les grants
        return md5($subject->getOwnerId() . $subject->getUpdatedAt()->format('U'));
    }

    public function apply(mixed $subject): PolicyResult
    {
        $resource = Resource::fromString('my:app:pet', (string) $subject->getId());

        $result = new PolicyResult($this->getName(), $resource);
        $result->etag = $this->computeEtag($subject);

        $result->addGrant(new PolicyResultGrant(
            principal: 'reef:iam:principal:' . $subject->getOwnerId(),
            allow: ['my:app:pet:perm:takeHome'],
        ));

        // Plusieurs grants possibles sur la même ressource
        // $result->addGrant(new PolicyResultGrant(
        //     principal: 'reef:iam:principals:veterinarians',
        //     allow: ['my:app:pet:perm:takeHome'],
        // ));

        return $result;
    }
}
```

## Implémentation d'une Policy "WithTeam"

Pour les entités BSM avec équipe, le bundle fournit des **classes abstraites de policy** qui pré-implémentent `canHandle()` et `subjectToString()` et enregistrent automatiquement le `ResourceFinder` correspondant dans le container Symfony.

### Classes abstraites disponibles

| Entité BSM | Classe abstraite |
|---|---|
| VeterinaryEstablishment | `NoahVet\Reef\Domain\Security\Policy\BSM\AbstractVeterinaryEstablishmentWithTeamPolicy` |
| VeterinaryGroup | `NoahVet\Reef\Domain\Security\Policy\BSM\AbstractVeterinaryGroupWithTeamPolicy` |
| Manufacturer | `NoahVet\Reef\Domain\Security\Policy\BSM\AbstractManufacturerWithTeamPolicy` |
| Publisher | `NoahVet\Reef\Domain\Security\Policy\BSM\AbstractPublisherWithTeamPolicy` |

### Ce que le bundle fournit automatiquement

Dès qu'une classe de votre projet étend une de ces classes abstraites et est détectée dans le container Symfony (taggée `reef.iam.policy`), le `WithTeamPoliciesCompilerPass` :

1. Crée une instance du `ResourceFinder` correspondant (ex : `VeterinaryEstablishmentWithTeamResourceFinder`)
2. Configure son `ResourceType` avec le slug retourné par `getResourceTypeSlug()`
3. Enregistre et tag le `ResourceFinder` avec `reef.iam.finder` pour qu'il soit utilisé par les use cases IAM

**Vous n'avez pas à déclarer le `ResourceFinder` dans vos `services.yaml`.**

### Comment implémenter une policy WithTeam

Exemple pour un établissement vétérinaire :

```php
use NoahVet\Reef\Domain\Business\Model\VeterinaryEstablishmentWithTeam;
use NoahVet\Reef\Domain\Security\Policy\BSM\AbstractVeterinaryEstablishmentWithTeamPolicy;
use NoahVet\Reef\Domain\Security\Policy\PolicyResult;
use NoahVet\Reef\Domain\Security\Policy\PolicyResultGrant;

class VetEstablishmentPolicy extends AbstractVeterinaryEstablishmentWithTeamPolicy
{
    /**
     * Slug IAM de votre type de ressource — doit correspondre à ce que vous avez
     * déclaré dans service_definition.json > externalResources.
     */
    public static function getResourceTypeSlug(): string
    {
        return 'reef:bsmApi:veterinaryEstablishment';
    }

    public function getName(): string
    {
        return 'vet-establishment-team-access';
    }

    /**
     * @param VeterinaryEstablishmentWithTeam $subject
     */
    public function computeEtag(object $subject): string
    {
        $hashContext = \hash_init('crc32c');
        foreach ($subject->getTeamMembers() as $member) {
            \hash_update($hashContext, $member->getUuid().'-'.$member->getJobTitle().';');
        }
        return \hash_final($hashContext);
    }

    /**
     * @param VeterinaryEstablishmentWithTeam $subject
     */
    public function apply(object $subject): PolicyResult
    {
        // $this->createResource() construit Resource::fromString(getResourceTypeSlug(), uuid)
        $resource = $this->createResource($subject);

        $result = new PolicyResult($this->getName(), $resource);
        $result->etag = $this->computeEtag($subject);

        foreach ($subject->getTeamMembers() as $member) {
            $result->addGrant(new PolicyResultGrant(
                principal: 'reef:iam:principal:'.$member->getUuid(),
                allow: ['my:app:veterinaryEstablishment:perm:readPrescriptions'],
            ));
        }

        return $result;
    }
}
```

### Méthodes fournies / à implémenter

| Méthode | Fournie par l'abstract | À implémenter | Description |
|---|---|---|---|
| `getResourceTypeSlug()` | — | Oui (statique) | Slug IAM ex. `reef:bsmApi:veterinaryEstablishment` |
| `getName()` | — | Oui | Identifiant unique de la policy dans l'IAM |
| `computeEtag()` | — | Oui | Hash de l'état de l'entité (évite les syncs inutiles) |
| `apply()` | — | Oui | Calcul des grants de permissions |
| `canHandle()` | Oui (`final`) | — | Vérifie que le sujet est bien du bon type |
| `subjectToString()` | Oui (`final`) | — | Retourne `getResourceTypeSlug():uuid` |
| `createResource()` | Oui (helper `final`) | — | Crée la `Resource` IAM pour l'entité |

> **Note sur `subjectToString()`** : retourne `getResourceTypeSlug():uuid` (ex : `reef:bsmApi:veterinaryEstablishment:abc-123`). Ce format est cohérent avec le slug produit par `Resource::fromString()`, ce qui garantit la compatibilité avec le stockage et la récupération des etags dans l'IAM.

---

## Synchronisation des policies (`reef:policies:sync`)

La commande `reef:policies:sync` parcourt toutes les entités exposées dans l'IAM, calcule leurs grants de permissions via les policies enregistrées, et publie les résultats vers l'IAM en évitant les envois inutiles grâce au mécanisme d'etag.

**En production, cette commande est exécutée chaque matin par le CRON.**

### Utilisation

```bash
# Synchronise toutes les ressources de tous les finders
php bin/console reef:policies:sync

# Synchronise une seule ressource par son slug IAM
php bin/console reef:policies:sync --resource=reef:bsmApi:veterinaryEstablishment:abc-123
php bin/console reef:policies:sync -r reef:bsmApi:veterinaryEstablishment:abc-123
```

### Comment les Finders sont utilisés

Le `ResourceFinderService` agrège tous les services taggués `reef.iam.finder` dans le container Symfony. Ces services implémentent l'une ou les deux interfaces de port :

| Interface | Rôle |
|---|---|
| `ResourceEnumeratorInterface` | `enumerate(): \Generator` — yield toutes les entités du type |
| `ResourceLocatorInterface` | `findBySlug(string $slug): mixed` — retrouve une entité par son slug IAM |

**Sync complète** (sans `--resource`) : le `ResourceFinderService::iterate()` appelle `enumerate()` sur chaque finder enregistré de manière séquentielle, en lazy-loading via `yield from`. Les entités arrivent une par une dans la boucle principale.

**Sync ciblée** (`--resource`) : le `ResourceFinderService::findBySlug($slug)` parcourt tous les finders qui implémentent `ResourceLocatorInterface` et retourne la première entité dont le finder reconnaît le préfixe du slug.

### Comment les Policies sont appelées

Le `PolicyOrchestrator` agrège tous les services taggués `reef.iam.policy`. Pour chaque entité produite par les finders :

1. `PolicyOrchestrator::getPoliciesFor($subject)` filtre les policies via `$policy->canHandle($subject)` et retourne celles applicables à l'entité
2. Pour chaque policy applicable, **l'etag est vérifié avant d'appliquer** :
   - `PolicyResultStore::findEtag($policyName, $subjectId)` → récupère l'etag actuellement stocké dans l'IAM
   - `$policy->computeEtag($subject)` → calcule l'etag courant de l'entité
   - **Si les etags diffèrent** : `$policy->apply($subject)` produit un `PolicyResult` qui est publié vers l'IAM
   - **Si les etags sont identiques** : la synchronisation est ignorée pour ce couple (policy, entité)

### Structure d'un PolicyResult

`$policy->apply()` retourne un `PolicyResult` qui contient :

- `policyName` — le nom de la policy (`getName()`)
- `subject` — la `Resource` ou `ResourceGroup` IAM concernée
- `etag` — le hash d'état à stocker (doit être assigné : `$result->etag = $this->computeEtag($subject)`)
- `grants` — liste de `PolicyResultGrant`, chacun définissant :
  - `principal` — le principal concerné (ex: `reef:iam:principal:{uuid}`)
  - `allow` — permissions accordées (ex: `['my:app:vet:perm:readPrescriptions']`)
  - `deny` — permissions explicitement refusées (optionnel)

### Flux complet

```
php bin/console reef:policies:sync
        │
        ├─[--resource fourni]──────────────────────────────────────┐
        │                                                          │
        │   ResourceFinderService::iterate()                       │   ResourceFinderService::findBySlug(slug)
        │   ├── Finder A: enumerate() → yield entité1, entité2…    │   ├── Finder A: findBySlug() → null
        │   ├── Finder B: enumerate() → yield entité3…             │   ├── Finder B: findBySlug() → entity ✓
        │   └── Finder C: enumerate() → …                          │   └── retourne entity
        │                                                          │
        └────────────────────┬─────────────────────────────────────┘
                             │ entity
                             ▼
             PolicyOrchestrator::getPoliciesFor(entity)
             ── canHandle(entity) ? ────────
             │ Policy1 ✓    │ Policy2 ✗    │ Policy3 ✓
             ▼              ▼              ▼
          applicable      ignorée       applicable

             Pour chaque policy applicable :
             │
             ├── store.findEtag(policyName, subjectId)  ← etag stocké dans l'IAM
             ├── policy.computeEtag(entity)             ← etag calculé maintenant
             │
             ├─[etags identiques]── rien à faire, passage suivant
             │
             └─[etags différents]──
                   │
                   ├── policy.apply(entity) → PolicyResult
                   │   ├── subject: Resource/ResourceGroup
                   │   ├── etag: hash courant
                   │   └── grants: [ {principal, allow, deny}, … ]
                   │
                   └── publisher.publish(PolicyResult) → IAM
```

### Optimisation batch (`BatchSyncPoliciesUseCase`)

La commande standard utilise `SyncPoliciesUseCase` (1 `findEtag` par couple policy × entité). Pour les **gros volumes** d'entités, injectez directement `BatchSyncPoliciesUseCase` dans votre propre code : il groupe les sujets par policy et appelle `store->findEtags(policyName, [...subjectIds])` en **un seul appel par policy**, réduisant drastiquement le nombre d'appels à l'IAM.

```php
use NoahVet\Reef\Application\Security\UseCase\BatchSyncPoliciesUseCaseInterface;

class SyncEstablishmentsCommand
{
    public function __construct(
        private readonly BatchSyncPoliciesUseCaseInterface $batchSync,
        private readonly EstablishmentRepository $repository,
    ) {}

    protected function execute(): int
    {
        // Charger les entités par lots (ex: 50 à la fois)
        foreach ($this->repository->findAllInBatches(50) as $batch) {
            $this->batchSync->batchSync($batch);
        }
        return Command::SUCCESS;
    }
}
```

### Quand déclencher la synchronisation

| Scénario | Recommandation |
|---|---|
| Synchronisation périodique | CRON quotidien via `reef:policies:sync` (déjà configuré) |
| Mise à jour d'une entité individuelle (listener Doctrine, webhook) | `SyncPoliciesUseCase::sync($entity)` |
| Resynchronisation forcée d'une resource unique | `reef:policies:sync -r <slug>` |
| Gros batch de mise à jour (import, migration) | `BatchSyncPoliciesUseCase::batchSync($entities)` |

---

## Vérification des permissions

Les permissions calculées par les policies et stockées dans l'IAM sont vérifiées au moment des requêtes entrantes.

### Via Symfony Security (recommandé)

Le `IAMVoter` est enregistré automatiquement dans Symfony Security. Il extrait le bearer token depuis le `ReefOAuthToken` et délègue au `PermissionCheckerInterface`.

```php
// Dans un Controller ou un Voter Symfony

/**
 * Vérifier une permission sur une Resource précise
 * 
 * Dans cet exemple on vérifie que le principal a bien la permission takeHome pour le pet donné
 */
$petResource = Resource::fromString('my:app:pet', (string) $pet->getId());
$this->denyAccessUnlessGranted('my:app:pet:perm:takeHome', $petResource);

/**
 * Vérification sur un type de resource
 *
 * Dans cet exemple, on vérifie que le principal a la permission pour AU MOINS 1 Pet (n'importe lequel)  
 */
$petType = ResourceType::fromString('my:app:pet');
$this->denyAccessUnlessGranted('my:app:pet:perm:takeHome', $petType);

/**
 * Vérifier qu'un utilisateur appartient à un groupe de principals
 * 
 *  Dans cet exemple on vérifie que le principal appartient au groupe des superAdmins
 */
$this->denyAccessUnlessGranted('reef:iam:principals:superAdmins');

/**
 * Vérifier à partir d'une entité Doctrine (mapping automatique via IAMResourceMapperInterface)
 */
$this->denyAccessUnlessGranted('my:app:pet:perm:takeHome', $petEntity);
```

> Si le sujet passé n'est ni un `Resource` ni un `ResourceType`, l'`IAMVoter` utilise l'`IAMResourceMapperInterface` pour le mapper vers un `Resource`. Si le mapper retourne `null`, le vote est `ACCESS_ABSTAIN`.

### Via PermissionCheckerInterface (hors Symfony)

Pour vérifier des permissions sans Symfony Security (ex: dans un use case, une commande CLI) :

```php
use NoahVet\Reef\Domain\Security\Port\PermissionCheckerInterface;
use NoahVet\Reef\Domain\Security\Model\Resource;

class MyUseCase
{
    public function __construct(
        private readonly PermissionCheckerInterface $permissionChecker,
    ) {}

    public function execute(string $petId, string $bearerToken): void
    {
        $resource = Resource::fromString('my:app:pet', $petId);

        if (!$this->permissionChecker->check($resource, ['my:app:pet:perm:takeHome'], $bearerToken)) {
            throw new AccessDeniedException();
        }

        // Vérification d'appartenance à un groupe
        if (!$this->permissionChecker->checkGroup('reef:iam:principals:veterinarians', $bearerToken)) {
            throw new AccessDeniedException();
        }
    }
}
```

---

## Tests

Lors des tests fonctionnels, le voter IAM est remplacé par un mock automatiquement. Pour l'utilisation, se référer à la documentation des tests OpenAPI :

→ [Documentation complète : Tests d'endpoints HTTP (OpenAPI)](endpoint-tests.md)

---

## Migration depuis l'ancienne API

Les classes sous `NoahVet\Reef\Security\Policy\*` sont **dépréciées**. Voici les équivalences :

| Ancien (deprecated) | Nouveau |
|---|---|
| `Security\Policy\PolicyResult(string $name, string $id, ResourceType $type)` | `Domain\Security\Policy\PolicyResult(string $name, Resource\|ResourceGroup $subject)` |
| `Security\Policy\PolicyResultGrant($resource, $principal, array $grants)` | `Domain\Security\Policy\PolicyResultGrant(principal: $p, allow: $a, deny: $d)` |
| `Security\Policy\AbstractPolicy` | `Domain\Security\Policy\AbstractPolicy` |
| `Security\Policy\PolicyInterface` | `Domain\Security\Policy\PolicyInterface` |

**Changements principaux :**

1. `PolicyResult` prend directement un objet `Resource|ResourceGroup` comme sujet (plus de string + ResourceType séparés).
2. `PolicyResultGrant` n'accepte plus de `$resource` en premier paramètre (la ressource est portée par `PolicyResult`). Utilisez les named arguments PHP pour sauter ce paramètre déprécié.
3. Les imports doivent pointer vers `NoahVet\Reef\Domain\Security\*` et non `NoahVet\Reef\Security\*`.
