API — Gestion de la licence¶
Cette page documente LicenseController, monté sous /api/license. Il gère l'activation, la révocation et la consultation de la licence commerciale PerfShop, qui débloque les niveaux avancés du chaos pour l'usage autonome.
Contrôleur couvert
LicenseController → GET /status, POST /activate, POST /revoke
Le modèle de licence PerfShop¶
PerfShop fonctionne en freemium : le parcours e-commerce complet (produits, panier, commandes, comptes, administration) est accessible en mode gratuit, mais les niveaux avancés du chaos en self-service étudiant (au-delà du niveau 1 pour la performance, au-delà du niveau 0 pour les autres familles) nécessitent une licence valide.
La frontière entre les deux modes est le code HTTP 402 Payment Required retourné par les endpoints étudiants quand un niveau dépasse la limite gratuite. Voir chaos-student.md.
Freemium vs pédagogique
Même sans licence, le formateur peut activer n'importe quel niveau de chaos via les endpoints /api/admin/chaos/* — il est identifié comme admin et n'est pas soumis au mur freemium. La licence protège uniquement le self-service étudiant et le pilotage des parcours pédagogiques (/pedagogique/activate, /pedagogique/join, /pedagogique/level).
Cette règle reflète l'usage réel de PerfShop : la licence est nécessaire pour les formateurs qui veulent que leurs étudiants s'entraînent en autonomie, pas pour les démonstrations ponctuelles où le formateur pilote lui-même.
Les trois plans commerciaux¶
PerfShop distingue trois plans qui débloquent différents ensembles de features :
| Plan | Identifiant interne | Interfaces débloquées |
|---|---|---|
| Functional Testing 🧪 | functional |
chaos-admin, admin, monitoring, scripts-ui |
| Performance Testing 🎯 | performance |
Functional + jmeter-ui |
| Enterprise 🏢 | enterprise |
Toutes les features y compris futures |
Quand aucune licence valide n'est active, le plan interne est none et toutes les interfaces protégées retournent 402 Payment Required.
Format des clés de licence¶
Les clés de licence PerfShop suivent un format standardisé :
La clé est composée de trois parties :
- Préfixe obligatoire :
PFSH- - Payload base64url : un objet JSON encodé décrivant la licence (
licenseId,holder,plan,issuedAt,expiresAt) - Signature base64url : séparée du payload par un point
., c'est une signature RSA-PSS 2048 bits avec SHA-256 calculée sur les octets ASCII du payload
Exemple de forme (pas une vraie clé) :
La validité d'une clé est vérifiée par le service LicenseService.parseLicense() qui :
- Vérifie le préfixe
PFSH- - Sépare le payload et la signature sur le dernier point
. - Charge la clé publique RSA embarquée dans le JAR backend
- Vérifie la signature RSA-PSS (
SHA-256+MGF1+ salt 32 bytes) - Décode le payload JSON et extrait les champs
Aucun appel réseau n'est nécessaire — la vérification est entièrement hors-ligne.
Protection d'intégrité de la clé publique¶
Le backend embarque la clé publique RSA officielle en constante source et vérifie son SHA-256 au démarrage (@PostConstruct). Si le hash ne correspond pas au hash attendu (EXPECTED_PUBLIC_KEY_HASH), la JVM refuse de démarrer avec une IllegalStateException. Cette vérification empêche un fork malveillant de PerfShop de substituer sa propre clé publique pour signer des licences frauduleuses — toute modification force la publication AGPL du fork.
Vue d'ensemble¶
| Méthode | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/license/status |
Aucune | Statut de la licence courante |
POST |
/api/license/activate |
Aucune 🔑 | Active une clé de licence |
POST |
/api/license/revoke |
Aucune 🔑 | Révoque la licence active |
Pourquoi /activate et /revoke sont publics
Les deux endpoints de mutation sont intentionnellement publics pour éviter un deadlock opérationnel :
/activate: sans licence valide, toutes les interfaces d'authentification protégées sont bloquées (HTTP 402). Si l'activation nécessitait une authentification, il serait impossible de s'authentifier pour activer sa première licence. L'endpoint est donc public, avec la sécurité assurée par la vérification RSA-PSS : une clé forgée est rejetée./revoke: avec une licence révoquée, l'auth admin est bloquée → deadlock si ce endpoint nécessitait une authentification. L'endpoint est public, et son impact est limité : sans licence valide, les interfaces sont simplement bloquées en 402. Une nouvelle activation nécessite une clé RSA valide.
GET /api/license/status¶
Retourne l'état courant de la licence. Endpoint fréquemment pollé par les interfaces chaos-admin, scripts-ui, jmeter-ui pour conditionner leur affichage avant toute tentative d'authentification.
Auth : aucune
Réponse — 200 OK (licence active)¶
{
"valid": true,
"plan": "performance",
"planLabel": "🎯 Performance Testing",
"holder": "Acme Training Corp",
"issuedAt": "2025-01-15",
"expiresAt": "2026-01-15",
"unlimited": false,
"daysRemaining": 282,
"features": [
"chaos-admin",
"admin",
"monitoring",
"scripts-ui",
"jmeter-ui"
]
}
| Champ | Description |
|---|---|
valid |
true si une licence est active et non expirée |
plan |
Identifiant interne du plan : functional, performance ou enterprise |
planLabel |
Libellé lisible du plan avec emoji |
holder |
Nom du bénéficiaire inscrit dans le payload signé |
issuedAt |
Date d'émission au format YYYY-MM-DD |
expiresAt |
Date d'expiration au format YYYY-MM-DD, ou null si licence illimitée |
unlimited |
true si expiresAt == null |
daysRemaining |
Nombre de jours restants jusqu'à l'expiration, ou null si illimitée. Plancher à 0 (pas de valeur négative) |
features |
Liste des interfaces débloquées par ce plan |
Réponse — 200 OK (aucune licence active)¶
{
"valid": false,
"plan": "none",
"planLabel": "Aucune licence",
"message": "Aucune licence valide. Activez votre licence via le panneau ci-dessous."
}
Les champs planLabel et message sont localisés via les clés license.status.none_label et license.status.none_message.
Cet endpoint ne retourne jamais d'erreur — même en cas de clé corrompue, d'expiration ou de problème interne, il répond 200 OK avec valid: false. C'est conçu pour être pollé en continu sans risque.
POST /api/license/activate¶
Active une clé de licence. La clé est vérifiée cryptographiquement par signature RSA-PSS, puis persistée dans la table perfshop_license de la base de données et chargée dans le cache mémoire.
Auth : aucune (voir justification plus haut)
Requête¶
| Champ | Type | Requis | Description |
|---|---|---|---|
licenseKey |
string | oui | Clé de licence complète au format PFSH-<payload>.<signature> |
Réponse — 200 OK¶
{
"success": true,
"message": "Licence activée avec succès",
"status": {
"valid": true,
"plan": "performance",
"planLabel": "🎯 Performance Testing",
"holder": "Acme Training Corp",
"issuedAt": "2025-01-15",
"expiresAt": "2026-01-15",
"unlimited": false,
"daysRemaining": 365,
"features": ["chaos-admin", "admin", "monitoring", "scripts-ui", "jmeter-ui"]
}
}
Le message de succès est localisé via license.activate.success. Le bloc status est identique à ce que retourne GET /status après activation.
Processus de validation¶
flowchart TB
A[POST /license/activate] --> B{Champ licenseKey présent ?}
B -->|non| E1[400 — license.error.key_empty]
B -->|oui| C{Commence par PFSH- ?}
C -->|non| E2[400 — license.error.key_format]
C -->|oui| D[LicenseService.activateLicense]
D --> F[Parse payload + vérif signature RSA-PSS]
F -->|signature invalide| E3[422 — license.error.invalid_signature]
F -->|date dépassée| E4[422 — license.error.expired]
F -->|OK| G[UPSERT perfshop_license]
G --> H[cachedLicense.set]
H --> I[200 OK]
Codes d'erreur¶
| Code | Corps | Cause |
|---|---|---|
| 400 | {"error": "Le champ 'licenseKey' est requis"} |
Champ absent ou vide (clé license.error.key_empty) |
| 400 | {"error": "Format invalide — la clé doit commencer par PFSH-"} |
Préfixe manquant (clé license.error.key_format) |
| 422 | {"error": "Clé invalide — signature RSA incorrecte ou format non reconnu"} |
Signature invalide, payload corrompu ou plan inconnu (clé license.error.invalid_signature) |
| 422 | {"error": "Licence expirée le 2024-12-31"} |
Date d'expiration dans le passé (clé license.error.expired) |
Code 422 pour les erreurs cryptographiques
Les erreurs de format (400) et les erreurs de vérification cryptographique (422) sont distinctes. Le 400 indique que la requête elle-même est malformée (champ manquant ou mauvais préfixe), le 422 indique que la requête est bien formée mais que le contenu de la clé n'est pas valide (signature incorrecte ou expiration dépassée). Cette distinction permet à un client de différencier une erreur de saisie utilisateur d'un rejet cryptographique.
Persistance en base de données¶
L'activation effectue un upsert sur la table perfshop_license avec la clé fixe id='current' :
-- Tentative de mise à jour (si la ligne existe déjà)
UPDATE perfshop_license
SET license_key=?, plan=?, holder=?,
issued_at=?, expires_at=?,
activated_at=NOW(), revoked=0
WHERE id='current';
-- Si UPDATE n'a touché aucune ligne, INSERT
INSERT INTO perfshop_license (id, license_key, plan, holder, issued_at, expires_at)
VALUES ('current', ?, ?, ?, ?, ?);
Une seule licence active par instance — les activations successives écrasent la précédente.
Priorité de chargement au démarrage¶
Au démarrage du backend (@PostConstruct), LicenseService.loadLicense() cherche la licence dans deux sources par ordre de priorité :
- Variable d'environnement
PERFSHOP_LICENSE_KEY(ou propriété Springperfshop.license.key) — utilisée pour les déploiements avec.env - Table
perfshop_licenseen base (ligneid='current'non révoquée) — utilisée après activation via l'API
Si aucune source ne fournit de licence valide, le cache reste vide et toutes les interfaces protégées retournent 402.
POST /api/license/revoke¶
Révoque la licence active : marque revoked=1 dans la table perfshop_license et vide le cache mémoire.
Auth : aucune (voir justification plus haut)
Requête¶
Pas de corps requis.
Réponse — 200 OK¶
{
"success": true,
"message": "Licence révoquée — interfaces protégées bloquées",
"status": {
"valid": false,
"plan": "none",
"planLabel": "Aucune licence",
"message": "Aucune licence valide. Activez votre licence via le panneau ci-dessous."
}
}
Le bloc status reflète l'état après révocation — identique à GET /status sans licence.
Codes d'erreur¶
| Code | Corps | Cause |
|---|---|---|
| 409 | {"error": "Aucune licence active à révoquer"} |
Aucune licence valide à révoquer (clé license.error.no_active) |
Effets de la révocation¶
- Tentative SQL :
UPDATE perfshop_license SET revoked = 1 WHERE id = 'current'. Si la mise à jour échoue (erreur DB), le message est loggé en WARN mais l'opération continue - Cache vidé :
cachedLicense.set(null)—isLicenseValid()retournefalseimmédiatement - Les chaos en cours ne sont pas interrompus (un chaos déjà actif continue de tourner jusqu'à son reset manuel)
- Les étudiants reçoivent
402 Payment Requiredsur leur prochaine tentative d'activation de niveau > freemium
Verrouillage des features par licence¶
Le service expose une méthode hasFeature(String feature) appelée par les contrôleurs pour vérifier si le plan courant donne accès à une fonctionnalité. Les règles sont :
| Plan | functional |
performance |
enterprise |
|---|---|---|---|
chaos-admin |
✅ | ✅ | ✅ |
admin |
✅ | ✅ | ✅ |
monitoring |
✅ | ✅ | ✅ |
scripts-ui |
✅ | ✅ | ✅ |
jmeter-ui |
❌ | ✅ | ✅ |
| autres | ❌ | ✅ | ✅ |
Quand un endpoint refuse l'accès pour cause de plan insuffisant, il retourne 402 avec le message générique license.error.feature_denied :
Le placeholder {0} est remplacé par l'identifiant de la feature refusée.
Exemple curl — activation et révocation¶
# 1. État initial — pas de licence
curl http://localhost:9080/api/license/status
# {
# "valid": false,
# "plan": "none",
# "planLabel": "Aucune licence",
# "message": "Aucune licence valide. ..."
# }
# 2. Tenter un niveau chaos étudiant → 402
curl -H "Content-Type: application/json" \
-d '{"level":3}' \
http://localhost:9080/api/chaos/student/performance
# { "error": "LICENSE_REQUIRED", "requested": 3, "maxFree": 1, ... }
# 3. Activer une licence Pro
curl -H "Content-Type: application/json" \
-d '{"licenseKey":"PFSH-eyJ...signaturexyz"}' \
http://localhost:9080/api/license/activate
# 4. Vérifier le statut après activation
curl http://localhost:9080/api/license/status
# { "valid": true, "plan": "performance", "daysRemaining": 365, ... }
# 5. Re-tenter le chaos niveau 3 → 200
curl -H "Content-Type: application/json" \
-d '{"level":3}' \
http://localhost:9080/api/chaos/student/performance
# 6. En fin de cours — révocation publique
curl -X POST http://localhost:9080/api/license/revoke
Sécurité de l'implémentation¶
PerfShop protège le mécanisme de licence contre plusieurs classes d'attaques :
Vérification hors-ligne par signature RSA-PSS¶
Aucun appel serveur externe n'est nécessaire pour valider une clé — la clé publique est embarquée dans le JAR backend. Cela garantit :
- Fonctionnement hors-ligne (salle de formation sans Internet)
- Aucune fuite d'IP des clients vers un serveur de licence externe
- Aucune dépendance à un service tiers
Anti-substitution de la clé publique¶
Le hash SHA-256 de la clé publique embarquée est vérifié au démarrage. Un fork de PerfShop qui substituerait une autre clé publique pour signer ses propres licences verrait le backend refuser de démarrer. Combiné à la licence AGPL, cela force la publication de toute modification et rend les licences frauduleuses détectables.
Pas de révélation de la clé¶
La clé activée est stockée en base et dans le cache, mais n'est jamais retournée par l'API :
GET /statusretourne le plan, le bénéficiaire, les dates — jamais la clé- Les logs n'incluent pas la clé en clair
- Une erreur d'activation n'expose pas la valeur saisie
Protection contre le replay¶
Une clé déjà utilisée peut être réactivée autant de fois que nécessaire — il n'y a pas de protection anti-replay côté backend. C'est intentionnel : les instances de formation sont reconstruites fréquemment, et obliger une révocation systématique compliquerait inutilement les workflows pédagogiques. Le contrôle commercial se fait en amont (cycle de renouvellement, facturation par poste).
Points d'attention¶
Pas d'arrêt automatique des chaos à l'expiration
Quand une licence arrive à expiration, le backend la considère automatiquement comme invalide à la prochaine validation — mais les chaos déjà actifs au moment de l'expiration continuent de tourner. C'est un compromis pour éviter qu'un cours en cours soit brutalement interrompu. Le formateur verra néanmoins le badge « licence expirée » dans le monitoring et devra renouveler dès que possible.
Une seule licence par instance
La table perfshop_license utilise une clé primaire fixe id='current' — il ne peut y avoir qu'une seule licence active à la fois. Activer une nouvelle clé écrase la précédente (y compris si elle était d'un plan supérieur). Pour basculer temporairement entre plans, il faut conserver les clés originales en dehors du système.
Liens associés¶
chaos-student.md— mur freemium détaillé- Système de licence — vue d'ensemble — détails cryptographiques RSA-PSS
admin.md— authentification admin bloquée en l'absence de licence