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

Ce module gère l'attribution des permissions aux utilisateurs (principals) sur les ressources du système.
Il s'appuie sur un service IAM externe via l'API Jane générée.

---

## Architecture (Hexagonale)

```
lib/Security/
├── Domain/          ← 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 que le Domain requiert de l'extérieur
│   └── Exception/   ← IAMException, PermissionFetchException
│
├── Application/     ← Use Cases orchestrant le Domain
│   ├── UseCase/     ← SyncPoliciesUseCase, BatchSyncPoliciesUseCase…
│   ├── Service/     ← PolicyOrchestrator, ResourceFinderService
│   └── Dto/         ← PolicySyncCommand (async)
│
└── Infrastructure/  ← Adaptateurs concrets
    ├── Api/         ← Appels à l'IAM via Jane (Publisher, Store, Registry…)
    ├── 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.

---

## Rôle de chaque composant

| Composant | Interface / Classe | Rôle |
|---|---|---|
| **Policy** | `Domain\Policy\PolicyInterface<T>` | Définit qui peut faire quoi sur quelles ressources pour un sujet `T` |
| **PolicyOrchestrator** | `Application\Service\PolicyOrchestrator` | Contient la liste des policies, filtre par `canHandle()` |
| **SyncPoliciesUseCase** | `Application\UseCase\SyncPoliciesUseCase` | Synchronise un sujet vers l'IAM (compare etag avant envoi) |
| **BatchSyncPoliciesUseCase** | `Application\UseCase\BatchSyncPoliciesUseCase` | Synchronise une liste de sujets (un seul appel API pour les etags) |
| **RegisterResourceUseCase** | `Application\UseCase\RegisterResourceUseCase` | Enregistre une ressource ou un groupe dans l'IAM |
| **Generator** | `Infrastructure\IAM\Generator\InternalResourceGeneratorInterface` | Crée les objets `Resource` / `ResourceGroupWithMembers` depuis une entité |
| **Finder** | `Infrastructure\Doctrine\AbstractDoctrineResourceFinder` | Énumère toutes les entités et retrouve une entité depuis son slug IAM |
| **IAMVoter** | `Infrastructure\Symfony\Voter\IAMVoter` | Voter Symfony qui délègue à `PermissionCheckerInterface` |
| **PermissionChecker** | `Domain\Port\PermissionCheckerInterface` | Vérifie si un bearer token a les permissions sur une ressource |

---

## Cas 1 — Votre projet doit déclarer de nouvelles ressources dans l'IAM

Vous créez une nouvelle entité (ex: `Pet`) et souhaitez l'exposer dans l'IAM.

### Étape 1 : Mettez à jour le fichier service_definition.json
Le fichier se trouve dans le dossier config/reef/service_definition.json

Exemple pour l'ajout de resources de type "Pet", ainsi qu'une permission "takeHome" :

```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 a ramener cet animal à la maison"
        }
      }
    }
  },
  "externalResources": {
  }
}
```

Dans cet exemple, type de resource disponible pour votre application est : *my:app:pet*. Une permission est créée avec le slug IAM *my:app:pet:perm:takeHome*.

### Étape 2 : Créer un Generator

Le Generator va assurer le mapping entre les entités de votre projet et les ressources IAM.

```php
// src/IAM/Generator/PetGenerator.php
use NoahVet\Reef\Security\Infrastructure\IAM\Generator\InternalResourceGeneratorInterface;
use NoahVet\Reef\Security\Domain\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());
    }
}
```

Dans cet exemple, un pet avec pour id "3" obtient le slug IAM *my:app:pet:3*.

### Étape 3 : Créer un Finder

Le finder est l'élément du système qui va permettre de trouver toutes les ressources d'un type donné pour les
enregistrer auprès de l'IAM. Il permet également de trouver une entité à partir de son slug IAM.

Une implémentation de Finder Doctrine est fournie.

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

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

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

### Étape 4 : Créer une Policy (optionnel)

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

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

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

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

    public function computeEtag(object $subject): string
    {
        return md5($subject->getOwnerId() . $subject->getUpdatedAt()->format('U'));
    }

    public function apply(object $subject): PolicyResult
    {
        $resourceType = ResourceType::fromString('my:app:pet');     
    
        $result = new PolicyResult(
            $this->getName(),
            $this->subjectToString($subject),
            $resourceType,
        );
        $result->etag = $this->computeEtag($subject);

        $resource = Resource::fromString($resourceType, (string) $subject->getId());

        $result->addGrant(new PolicyResultGrant(
            $resource,
            'reef:iam:principal:'.$subject->getOwnerId(), // principal UUID
            [ 'my:app:pet:perm:takeHome' ], // Slug IAM de la permission
        ));
        
        // Il est possible de donner plusieurs permissions sur plusieurs resources du même type ici

        return $result;
    }
}
```

## Cas 2 — Assignation de permissions sur une resource externe

Si vous souhaitez assigner des permissions sur une resource externe (ex: une ressource depuis un autre projet),
suivez les étapes suivantes :

### Étape 1 : Mettez à jour le 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 à toutes les ordonnances émises par une clinique vétérinaire",
          "description": "Seules le personnel habilité peut lire ces ordonnances envoyées par leur clinique de référence"
        }
      }
    }
  }
}
```

### Étape 2 : Création du Finder

Dans ce cas, pas besoin d'un Generator, puisque votre application ne génère aucune ressource. Elle doit juste pouvoir
trouver les ressources existantes.

```php
use NoahVet\Reef\Business\Enumerator\EstablishmentWithTeamEnumerator;

/**
 * Enumerate veterinary establishments from the BSM via the API 
 */
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
    {
        if (!\str_starts_with($slug, self::IAM_PREFIX)) {
            return null;
        }

        $uuid = \str_replace(self::IAM_PREFIX, '', $slug);

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

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

### Étape 3 : Créer une Policy (optionnel)

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

/** @extends AbstractPolicy<EstablishmentWithTeam> */
class PetPolicy implements PolicyInterface
{
    public const RESOURCE_TYPE_SLUG = 'reef:bsmApi:veterinaryEstablishment';

    public function getName(): string { return 'prescription-reader'; }

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

    public function subjectToString(object $subject): string { return (string) $subject->getUuid(); }

    public function computeEtag(object $subject): string
    {
        $hashContext = \hash_init('crc32c');

        \hash_update($hashContext, 'version-1;');

        foreach ($subject->getTeamMembers() as $teamMember) {
            \hash_update($hashContext, $teamMember->getUuid().'-'.$teamMember->getJobTitle().';');
        }

        $establishmentOptions = $this->reefBusinessUserAdapter->findEstablishmentServices([$subject->getUuid()]);
        $hasNego = 1 <= \count(
            \array_filter(
                $establishmentOptions,
                fn (ServiceOptionVetinWeb $option) => 'NGO-AM' === $option->optionCode,
            ),
        );
        \hash_update($hashContext, 'nego='.($hasNego ? 'true' : 'false').';');

        return \hash_final($hashContext);
    }

    public function apply(object $subject): PolicyResult
    {
        $uid = $this->subjectToString($subject);
        $resourceType = ResourceType::fromString(self::RESOURCE_TYPE_SLUG);

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

        $resource = Resource::fromString($resourceType, $subject->getUuid());

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

        return $result;
    }
}
```

---

## 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)
