# SECURITY.md

Ce que l'application protège, comment, et ce qu'elle ne protège pas.

## 1. Ce qui est en jeu

L'application stocke des **noms, prénoms, adresses e-mail, numéros de
téléphone et photographies de personnes identifiées**. Une photo associée à la
mauvaise personne, ou un portail laissé ouvert, n'est pas un bug d'affichage :
c'est une violation de données.

Deux surfaces très différentes cohabitent :

| Surface | Accès | Exposition |
|---|---|---|
| Application | compte obligatoire, 2 rôles | interne, équipe restreinte |
| **Portail participant** | **aucun compte** | **publique** |

Tout le reste de ce document découle de cette asymétrie.

## 2. Authentification

- Mots de passe : **12 caractères minimum**, lettres et chiffres, hachés par
  bcrypt. En production, contrôle supplémentaire contre les fuites connues
  (`Password::uncompromised()`).
- **5 tentatives par minute** et par couple (e-mail, IP) ; au-delà, blocage
  avec délai annoncé.
- Un compte désactivé perd l'accès **à la requête suivante**, sans attendre
  l'expiration de sa session — c'est le seul moyen de couper réellement l'accès
  à un photographe en cours d'événement.
- Session régénérée à la connexion, invalidée à la déconnexion.

## 3. Autorisations

Deux rôles seulement : `super_admin` et `photographer`. Un troisième rôle
n'apporterait rien qu'une policy ne sache déjà exprimer.

Le photographe ne voit **que** les shootings auxquels il est affecté, et
seulement ceux qui sont exploitables (ni brouillon, ni archivé). Toute requête
est bornée par `scopeVisibleTo()`.

> ⚠️ **Piège documenté.** `Gate::before` accorde tout au super administrateur
> *avant* que les policies ne s'exécutent. Une règle qui doit s'appliquer
> **aussi** à lui — « on ne peut pas désactiver son propre compte » — ne serait
> donc jamais atteinte. Les abilities concernées sont listées explicitement
> dans `AppServiceProvider::ABILITIES_WITHOUT_SUPER_ADMIN_BYPASS`.

Actions réservées au super administrateur : création et modification des
shootings, gestion des clients et des photographes, import, suppression de
photos, effacement RGPD.

## 4. Portail participant

Quatre verrous indépendants, chacun testé :

1. **Adresse non devinable** — jeton de 32 caractères aléatoires. Un portail
   inconnu et un portail désactivé renvoient le **même 404**, pour ne pas
   confirmer l'existence d'un événement.
2. **Code d'accès facultatif** — haché, **10 essais par quart d'heure**. Un
   code court sans limitation serait cassé en quelques minutes.
3. **Anti-énumération** — 3 caractères minimum, **5 résultats maximum**, aucune
   liste sans terme de recherche, et le total n'est jamais annoncé.
4. **Quota par origine** — 10 recherches/minute, 60/heure, 200/jour.

### Livraison des images

Aucune image n'est joignable par une URL devinable. Les disques sont **hors du
document root** et `storage:link` n'est jamais utilisé pour les photos.

Le portail ne sert que par **URL signée valable 10 minutes**, et uniquement si
la photo est **publiée**. Dépublier une photo la retire instantanément, même
pour quelqu'un qui détient encore un lien.

> ⚠️ **La signature couvre l'hôte complet.** Si `APP_URL` ne correspond pas
> exactement à l'adresse réellement visitée (`http` au lieu de `https`, avec ou
> sans `www`), *toutes* les images du portail répondent 403 alors que le reste
> de l'application fonctionne. `php artisan app:doctor` le vérifie.

## 5. En-têtes HTTP

Appliqués à **toutes** les réponses par le middleware `SecurityHeaders` :

| En-tête | Valeur |
|---|---|
| `X-Robots-Tag` | `noindex, nofollow, noarchive, noimageindex` |
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `DENY` |
| `Referrer-Policy` | `same-origin` |
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` |
| `Content-Security-Policy` | voir ci-dessous |

**La CSP n'est pas stricte, et il faut le dire.** Alpine.js évalue les
expressions de ses attributs à l'exécution : `'unsafe-eval'` est indispensable,
sans contournement possible à moins d'abandonner Alpine. Ce que la politique
apporte malgré tout — et qui compte ici — c'est qu'**aucune ressource externe
ne peut être chargée et aucune donnée exfiltrée vers un autre domaine**. Les
portraits ne quittent pas ce serveur.

## 6. Traçabilité

`shooting_events` est un journal **en ajout seul** : jamais d'`UPDATE`, jamais
de `DELETE`. Il consigne les prises de vue, les annulations, les notes, les
mouvements de photos, les imports, les purges et les effacements — avec auteur,
horodatage et adresse IP.

Les colonnes de `participants` n'en sont qu'un cache de lecture, reconstructible
à tout moment par `recomputeShotState()`.

Le portail dispose de son propre journal, `portal_access_logs` : recherches,
codes refusés, téléchargements. **L'adresse IP y est hachée avec un sel
applicatif** — on peut compter et corréler, jamais remonter à une personne.

## 7. RGPD

| Droit | Mise en œuvre |
|---|---|
| Effacement (art. 17) | `DataRetentionService::forgetParticipant()` — bouton réservé au super administrateur |
| Limitation de conservation (art. 5) | `purge_after` calculé à la fin du shooting, purge quotidienne automatique |
| Portabilité (art. 20) | exports CSV, Excel et PDF |
| Minimisation | IP hachées, métadonnées EXIF retirées des dérivés, fichier d'import supprimé après usage |

**La purge efface les personnes, pas les chiffres.** Identités, e-mails,
téléphones, photos et notes disparaissent ; le nombre de participants, la
cadence et la répartition par photographe subsistent — ce sont des données de
production sans caractère personnel. Le rapport client reste donc consultable
des années plus tard sans conserver le moindre visage.

Un import abandonné en cours d'assistant laisse un tableur de données
personnelles sur le serveur : `app:prune-imports` le supprime au bout de deux
jours.

## 8. Ce qui n'est pas couvert

Dit franchement, pour éviter toute fausse assurance :

- **Pas de double authentification.** Sur une équipe de quelques photographes,
  le rapport coût/bénéfice ne la justifiait pas en V1.
- **Pas de chiffrement au repos des photos.** Elles sont protégées par les
  droits du système de fichiers et l'absence d'URL publique, pas par un
  chiffrement. Un accès au serveur donne accès aux images.
- **Pas de détection d'intrusion ni d'alerte automatique** en cas de
  comportement anormal sur le portail. Les journaux permettent le constat
  *a posteriori*, pas la réaction en temps réel.
- **Le code d'accès du portail est partagé** entre tous les participants d'un
  événement : il freine la curiosité, il ne résiste pas à une fuite interne.
- **La CSP autorise `unsafe-eval`** (voir §5).

## 9. En cas d'incident

1. Désactiver le portail du shooting concerné (fiche shooting → Portail).
2. **Régénérer l'adresse du portail** : l'ancienne cesse immédiatement de
   fonctionner pour tout le monde.
3. Dépublier les photos concernées — l'accès est coupé instantanément, même
   pour les liens déjà distribués.
4. Consulter `portal_access_logs` pour évaluer l'étendue des accès.
5. Si des données personnelles ont été exposées : notification CNIL sous
   72 heures.
