corentin_wakdo/docs/uml/security-sequence.md
Corentin JOGUET 32ff6a63ba
All checks were successful
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 16s
CI / static-tests (push) Successful in 4s
P1 conception: security-by-design layer (Merise 21 entities, Forgejo CI/CD, hardening) (#3)
2026-06-15 12:16:11 +02:00

232 lines
12 KiB
Markdown

# Diagramme de sequence securite - Annulation de commande avec PIN (CANCEL_ORDER)
**Phase UML** : P1 - Conception, complement UML (passe security-by-design)
**Statut** : v0.2 - flux sensible PIN-gate + audit_log (controle anti-fraude interne)
**Date** : 2026-06-12
**Branche** : `feat/p1-conception`
**Auteur methodologie** : BYAN
---
## 1. Objet du document
Ce document decrit le **flux temporel securise** de l'annulation d'une commande
en back-office (`CANCEL_ORDER`). L'annulation est une action de **manipulation
d'argent** : annuler une commande deja `paid` peut servir a masquer un
detournement d'especes (l'equipier encaisse, annule, garde le cash). Le flux
ci-dessous materialise le controle qui adresse ce risque : une **re-authentification
par PIN par equipier** (`RG-T13`) avant l'execution, et l'ecriture d'une ligne
**`audit_log` immuable** dans la meme transaction que l'effet (`RG-T14`), de sorte
que chaque annulation est rattachee a une personne meme sur un poste partage.
Le diagramme reste au niveau conceptuel / logique. Il nomme les echanges entre
participants sans detailler l'implementation PHP ni le SQL exact. Il complete
l'operation `CANCEL_ORDER` du `docs/merise/mlt.md` (7.1), la transition T5 de
`docs/uml/state-commande.md` (`paid -> cancelled`, re-credit du stock) et le cas
d'utilisation "Annuler une commande" de `docs/uml/use-cases.md` (UC13).
**Sources** :
- `docs/merise/mlt.md` 7.1 `CANCEL_ORDER` (PRE-1..PRE-3, RG-1..RG-6, POST, ERR)
- `docs/merise/mlt.md` section 2 : `RG-T13` (PIN sensible), `RG-T14` (audit_log), `RG-T11` (re-credit dans la meme transaction), `RG-T07` (garde de concurrence), `RG-T08` (transaction atomique)
- `docs/merise/dictionary.md` 3.14 (`user.pin_hash`, argon2id) et 3.20 (`audit_log`)
- `docs/uml/state-commande.md` T5 (`paid -> cancelled`, `stock_movement` type `cancellation`)
---
## 2. Participants
| Participant | Role | Couche |
|---|---|---|
| **Equipier** | Counter / Drive / Admin titulaire de `order.cancel`, sur poste partage | Acteur |
| **Admin UI** | Interface back-office (Bloc 3, formulaire d'annulation + saisie PIN) | Presentation |
| **Controleur** | Back-end REST sous `/api/*` (Bloc 2), orchestre la transaction | Application |
| **Verif PIN** | Service de verification du PIN (`password_verify` argon2id) | Application |
| **BDD** | Base de donnees MariaDB | Persistance |
La session est **partagee par poste de travail** pour le routine 95% ; le PIN
re-introduit une attribution individuelle sur le sous-ensemble sensible
(`mlt.md` `RG-T13`). Le PIN n'est pas une session : il est verifie a chaque action
sensible et sert a capturer le `user_id` qui sera ecrit dans `audit_log`.
---
## 3. Diagramme de sequence
```mermaid
sequenceDiagram
actor Equipier
participant AdminUI as Admin UI
participant Ctrl as Controleur
participant PIN as Verif PIN
participant BDD
Note over Equipier,BDD: Phase 1 - Demande et controle de permission
Equipier->>AdminUI: demander l'annulation d'une commande
AdminUI->>Ctrl: POST /api/orders/{id}/cancel
Ctrl->>Ctrl: verifier session active + is_active = 1 (RG-T02)
Ctrl->>BDD: lire role_permission (permission order.cancel ?) (RG-T03, PRE-1)
BDD-->>Ctrl: permission presente
Ctrl->>BDD: lire customer_order (existe ? statut ?) (PRE-2, PRE-3)
BDD-->>Ctrl: status courant
alt status hors ['pending_payment','paid'] (delivered ou cancelled)
Ctrl-->>AdminUI: 422 CANNOT_CANCEL_IN_STATE {current_status}
AdminUI-->>Equipier: refus, aucun changement d'etat (ERR-1)
else status annulable
Note over Equipier,BDD: Phase 2 - Re-authentification PIN (RG-T13)
Ctrl-->>AdminUI: demander la saisie du PIN
Equipier->>AdminUI: saisir le PIN
AdminUI->>Ctrl: soumettre le PIN (re-auth action sensible)
Ctrl->>PIN: verifier le PIN soumis
PIN->>BDD: lire user.pin_hash de l'equipier
BDD-->>PIN: pin_hash (argon2id)
PIN->>PIN: password_verify(pin, pin_hash)
alt PIN incorrect
PIN-->>Ctrl: echec
Ctrl-->>AdminUI: refus du PIN, action rejetee
AdminUI-->>Equipier: PIN incorrect, aucun changement d'etat
else PIN correct
PIN-->>Ctrl: succes, acting user_id capture (RG-T13)
Note over Equipier,BDD: Phase 3 - Transaction atomique (RG-T08, RG-T11)
Ctrl->>BDD: BEGIN transaction
Ctrl->>BDD: UPDATE customer_order SET status = 'cancelled', cancelled_at = NOW() WHERE id = :id AND status IN ('pending_payment','paid') (RG-1, RG-T07)
BDD-->>Ctrl: lignes affectees
alt 0 ligne affectee (annulation concurrente)
Ctrl->>BDD: ROLLBACK
Ctrl-->>AdminUI: 409 INVALID_TRANSITION (ERR-2)
AdminUI-->>Equipier: deja annulee par un autre poste
else 1 ligne affectee
opt statut anterieur = paid (re-credit du stock)
Ctrl->>BDD: UPDATE ingredient SET stock_quantity = stock_quantity + :units (par ingredient consomme) (RG-3)
Ctrl->>BDD: INSERT stock_movement (type cancellation, delta +units, order_id, user_id de l'equipier) (RG-3)
end
Ctrl->>BDD: INSERT audit_log (action_code order.cancel, actor_user_id, actor_role_id, entity_type customer_order, entity_id, summary [statut anterieur + montant re-credite]) (RG-6, RG-T14)
Ctrl->>BDD: COMMIT
BDD-->>Ctrl: transaction validee
Ctrl-->>AdminUI: 200 annulation confirmee (OUT-1)
AdminUI-->>Equipier: commande annulee, trace enregistree
end
end
end
```
---
## 4. Notes de modelisation : chaque pas et sa regle
Le tableau ci-dessous mappe chaque interaction du diagramme a la regle
`mlt.md` 7.1 ou a la regle transverse correspondante, et a l'entite ecrite.
| # | Interaction | Regle (mlt.md) | Entite ecrite / lue |
|---|---|---|---|
| 1 | Verifier session active + `is_active = 1` | `RG-T02` | `user` (lecture) |
| 2 | Verifier `order.cancel` via `role_permission` | `RG-T03`, 7.1 PRE-1 | `role_permission` (lecture) |
| 3 | Charger la commande et lire son `status` | 7.1 PRE-2, PRE-3 | `customer_order` (lecture) |
| 4 | Bloquer si `status` est `delivered` ou `cancelled` | 7.1 ERR-1 | aucune ecriture (HTTP 422) |
| 5 | Demander + verifier le PIN (`password_verify` argon2id) | `RG-T13`, 7.1 RG-6 | `user.pin_hash` (lecture) |
| 6 | Rejeter si PIN incorrect, sans changement d'etat | `RG-T13` | aucune ecriture |
| 7 | Capturer l'`acting user_id` pour l'audit | `RG-T13` | (en memoire, sert aux pas 11-12) |
| 8 | `BEGIN` transaction | `RG-T08` | transaction |
| 9 | `UPDATE customer_order SET status='cancelled'` avec garde `AND status IN (...)` | 7.1 RG-1, `RG-T07` | `customer_order` (ecriture) |
| 10 | `ROLLBACK` + 409 si 0 ligne affectee (concurrence) | 7.1 ERR-2, `RG-T07` | aucune ecriture nette |
| 11 | Re-credit conditionnel du stock si statut anterieur `paid` | 7.1 RG-3, `RG-T11` | `ingredient`, `stock_movement` (type `cancellation`) |
| 12 | `INSERT audit_log` dans la meme transaction | 7.1 RG-6, `RG-T14` | `audit_log` (ecriture) |
| 13 | `COMMIT` (tout ou rien) | `RG-T08`, `RG-T11` | transaction |
| 14 | Reponse 200 de confirmation | 7.1 OUT-1 | aucune ecriture |
### 4.1 Re-credit conditionnel du stock (`RG-T11`)
Le re-credit ne s'applique que si la commande etait au statut `paid` avant
l'annulation (7.1 RG-3). Une commande `pending_payment` n'avait pas encore
decremente le stock (le decrement a lieu a la transition `paid`), donc il n'y a
rien a re-crediter. Pour chaque `order_item` d'une commande `paid`, les unites
consommees sont recalculees (format `normal`/`maxi`, ajustees par les
`order_item_modifier`), `ingredient.stock_quantity` est re-incremente et un
`stock_movement` de type `cancellation` est insere. `RG-T11` garantit que ce
re-credit et l'`UPDATE` du statut sont dans la **meme transaction** : il n'y a pas
de decrement orphelin si une etape echoue.
### 4.2 Garde de concurrence (`RG-T07`)
L'`UPDATE` porte la clause `AND status IN ('pending_payment','paid')`. Si deux
postes tentent d'annuler la meme commande au meme instant, seul le premier
obtient une ligne affectee ; le second recoit 0 ligne et le controleur repond
409 `INVALID_TRANSITION` apres `ROLLBACK` (7.1 ERR-2). Cette garde optimiste
reduit le risque d'une double annulation et d'un double re-credit du stock.
### 4.3 PIN distinct de la session (`RG-T13`)
La session reste **partagee par poste** pour le flux routine. Le PIN est verifie
a chaque action du sous-ensemble sensible (annulation, prix/VAT, RBAC, gestion
utilisateur, correction d'inventaire), et c'est lui qui fournit l'`actor_user_id`
ecrit dans `audit_log`. Le `pin_hash` est un hash argon2id (`dictionary.md` 3.14),
compare via `password_verify` ; il fait partie des champs RESTRICTED tenus hors
des logs et des reponses API.
---
## 5. Menace adressee : repudiation et detournement d'especes
L'annulation d'une commande `paid` est le geste qui permet le schema de fraude
"encaisser puis annuler pour garder le cash" (insider cash-skim). Sans controle,
sur un poste a session partagee, une annulation ne serait rattachee a personne :
l'auteur pourrait nier l'avoir faite (repudiation). Le flux ci-dessus reduit le
risque de ce schema en combinant deux mecanismes concrets :
- **PIN par equipier (`RG-T13`)** : l'annulation exige une re-authentification
individuelle. Sur un poste partage, cela rattache l'acte a une personne et non
au seul poste. Le PIN tend a dissuader l'usage opportuniste d'une session
laissee ouverte par un collegue.
- **`audit_log` immuable (`RG-T14`)** : chaque annulation ecrit une ligne
`audit_log` (`action_code = order.cancel`, `actor_user_id`, `actor_role_id`,
`entity_type`, `entity_id`, `summary` avec le statut anterieur et le montant
re-credite) dans la **meme transaction** que l'`UPDATE` du statut. La table
n'accepte ni `UPDATE` ni `DELETE` au niveau applicatif (`dictionary.md` 3.20).
Une annulation ne peut donc pas exister sans sa trace, et la trace ne peut pas
etre effacee par l'auteur.
L'effet combine : un pic d'annulations rattachees a un meme `actor_user_id`
devient visible et opposable lors d'une revue. Ceci ne supprime pas le risque,
mais le **reduit** en transformant un acte anonyme et niable en un acte attribue
et trace. La residualite (collusion, partage de PIN) releve de controles
organisationnels hors du modele de donnees.
> Note : `audit_log` enregistre des **noms de champs** et un `summary`
> non-personnel (`details` stocke les noms de champs modifies, pas de PII),
> conformement a `RG-T14` et a la classification de `PROJECT_CONTEXT.md` section 19.
> L'attribution `stock_movement.user_id` du re-credit complete la trace cote stock
> sans double journalisation.
---
## 6. Coherence avec les autres livrables
| Verification | Resultat |
|---|---|
| Statuts annulables coherents avec `state-commande.md` | Oui : `pending_payment` et `paid` (T3, T5) ; `delivered` non annulable (7.1 ERR-1) |
| Transition `paid -> cancelled` avec re-credit | Couverte par T5 et 7.1 RG-3 (`stock_movement` type `cancellation`) |
| Entites ecrites presentes au dictionnaire | `customer_order` (3.10), `ingredient`, `stock_movement`, `audit_log` (3.20) |
| Regle PIN appliquee | `RG-T13` (sous-ensemble sensible inclut 7.1) ; `user.pin_hash` (3.14) |
| Regle audit appliquee | `RG-T14` ; colonnes conformes a `audit_log` (3.20) |
| Atomicite re-credit + statut + audit | `RG-T08` + `RG-T11` (une transaction, `COMMIT` / `ROLLBACK`) |
| Codes d'erreur | 422 `CANNOT_CANCEL_IN_STATE` (ERR-1), 409 `INVALID_TRANSITION` (ERR-2) |
---
## 7. Arbitrage tranche
Le flux retient la re-authentification **par PIN** plutot qu'une re-saisie du mot
de passe complet : le PIN couvre le sous-ensemble sensible sans casser le routine
95% a session partagee (`RG-T13`), tout en fournissant l'attribution
individuelle. L'`audit_log` est ecrit dans la **meme transaction** que l'effet
(`RG-T14` + `RG-T08`) : une annulation sans trace ne peut pas etre committee. Le
re-credit du stock est conditionnel au statut anterieur `paid` (`RG-T11`), ce qui
ecarte un re-credit indu sur une commande `pending_payment` qui n'a pas ete
decrementee. Les codes d'erreur (`CANNOT_CANCEL_IN_STATE`, `INVALID_TRANSITION`)
reprennent ceux de `mlt.md` 7.1, sans en inventer de nouveaux.