Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
383 lines
14 KiB
Markdown
383 lines
14 KiB
Markdown
# Story 3.2: Révocation API Key (User)
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
En tant qu'**Utilisateur Pro**,
|
|
Je veux **révoquer ma propre clé API**,
|
|
de sorte que **je puisse sécuriser mon compte si la clé est compromise**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Endpoint DELETE**: `DELETE /api/v1/api-keys/{key_id}` révoque la clé API spécifiée. (FR30)
|
|
2. **Propriété Vérifiée**: Seules les clés appartenant à l'utilisateur connecté peuvent être révoquées.
|
|
3. **Révocation Immédiate**: La clé est marquée `is_active=False` et les requêtes suivantes avec cette clé retournent 401 avec le code `API_KEY_REVOKED`.
|
|
4. **Réponse Confirmée**: Retourne 200 avec confirmation de révocation dans le format `{data: {...}, meta: {}}`.
|
|
5. **Clé Introuvable**: Si la clé n'existe pas ou n'appartient pas à l'utilisateur, retourne 404 avec `API_KEY_NOT_FOUND`.
|
|
6. **Restriction Tier**: Les utilisateurs Free reçoivent 403 avec le code `PRO_FEATURE_REQUIRED`.
|
|
7. **Authentification Requise**: Les utilisateurs non authentifiés reçoivent 401 avec `UNAUTHORIZED`.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Implémenter l'Endpoint DELETE** (AC: #1, #2, #3, #4)
|
|
- [x] 1.1 Ajouter la route `DELETE /api/v1/api-keys/{key_id}` dans `routes/api_key_routes.py`
|
|
- [x] 1.2 Vérifier que l'utilisateur est authentifié et Pro
|
|
- [x] 1.3 Rechercher la clé par `id` ET `user_id` (sécurité propriété)
|
|
- [x] 1.4 Si trouvée, définir `is_active=False` et sauvegarder
|
|
- [x] 1.5 Retourner 200 avec confirmation `{data: {id, revoked: true}, meta: {}}`
|
|
|
|
- [x] **Task 2: Gérer les Cas d'Erreur** (AC: #5, #6, #7)
|
|
- [x] 2.1 Retourner 404 si clé non trouvée ou n'appartient pas à l'utilisateur
|
|
- [x] 2.2 Retourner 403 si utilisateur Free
|
|
- [x] 2.3 Retourner 401 si non authentifié
|
|
|
|
- [x] **Task 3: Vérifier l'Impact sur l'Authentification API** (AC: #3)
|
|
- [x] 3.1 Vérifier que le middleware d'authentification API vérifie `is_active`
|
|
- [x] 3.2 Si non vérifié, ajouter la vérification dans le middleware/fonction d'auth API
|
|
|
|
- [x] **Task 4: Ajouter les Tests** (AC: Tous)
|
|
- [x] 4.1 Test révocation réussie pour utilisateur Pro
|
|
- [x] 4.2 Test révocation échoue pour clé d'un autre utilisateur (404)
|
|
- [x] 4.3 Test révocation échoue pour utilisateur Free (403)
|
|
- [x] 4.4 Test révocation échoue sans authentification (401)
|
|
- [x] 4.5 Test clé révoquée ne peut plus authentifier (401 avec API_KEY_REVOKED)
|
|
|
|
## Dev Notes
|
|
|
|
### Infrastructure Existante (Ne pas réimplémenter)
|
|
|
|
**Routeur API Keys** (`routes/api_key_routes.py`):
|
|
- POST `/api/v1/api-keys` - Création de clé ✅ Existant
|
|
- GET `/api/v1/api-keys` - Liste des clés ✅ Existant
|
|
- DELETE `/api/v1/api-keys/{key_id}` - **À implémenter** (cette story)
|
|
|
|
**Modèle ApiKey** (`database/models.py` lignes 208-257):
|
|
```python
|
|
class ApiKey(Base):
|
|
__tablename__ = "api_keys"
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(String(36), ForeignKey("users.id"))
|
|
name = Column(String(100))
|
|
key_hash = Column(String(255))
|
|
key_prefix = Column(String(10))
|
|
is_active = Column(Boolean, default=True) # ⭐ Champ à modifier pour révocation
|
|
scopes = Column(JSON, default=list)
|
|
last_used_at = Column(DateTime)
|
|
usage_count = Column(Integer, default=0)
|
|
created_at = Column(DateTime)
|
|
expires_at = Column(DateTime)
|
|
```
|
|
|
|
**Dépendance Auth Pro** (`routes/api_key_routes.py`):
|
|
```python
|
|
def _require_pro_user(credentials=Depends(security)):
|
|
"""Dependency that requires a valid Pro user JWT token"""
|
|
# Vérifie JWT, récupère user, vérifie tier
|
|
# Retourne user ou None
|
|
```
|
|
|
|
### Patterns à Suivre (depuis Story 3.1)
|
|
|
|
**Format de Réponse Succès**:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "abc123",
|
|
"revoked": true,
|
|
"revoked_at": "2024-01-15T10:30:00Z"
|
|
},
|
|
"meta": {}
|
|
}
|
|
```
|
|
|
|
**Format de Réponse Erreur**:
|
|
```json
|
|
{
|
|
"error": "API_KEY_NOT_FOUND",
|
|
"message": "Clé API non trouvée ou n'appartient pas à l'utilisateur"
|
|
}
|
|
```
|
|
|
|
**Pattern de Query avec Propriété**:
|
|
```python
|
|
# Toujours filtrer par user_id pour sécurité
|
|
api_key = session.query(ApiKey).filter(
|
|
ApiKey.id == key_id,
|
|
ApiKey.user_id == user.id # ⭐ Sécurité: seul propriétaire peut révoquer
|
|
).first()
|
|
```
|
|
|
|
### Structure de Fichiers
|
|
|
|
```
|
|
routes/
|
|
├── api_key_routes.py # MODIFIER - Ajouter DELETE endpoint
|
|
├── auth_routes.py # Existant - patterns à suivre
|
|
└── translate_routes.py # Existant
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
- Le projet suit une structure plate (pas de dossier `backend/app/`)
|
|
- Les modèles sont dans `database/models.py`
|
|
- Les repositories sont dans `database/repositories.py`
|
|
- Les services sont dans `services/`
|
|
- Les tests sont dans `tests/`
|
|
|
|
### Références
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.2]
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
|
|
- [Source: database/models.py#ApiKey]
|
|
- [Source: routes/api_key_routes.py#_require_pro_user]
|
|
|
|
## Intelligence de la Story Précédente (3.1)
|
|
|
|
### Ce qui a été implémenté
|
|
|
|
1. **Routeur API Keys** avec POST et GET
|
|
2. **Génération sécurisée** avec `secrets.token_urlsafe(32)`
|
|
3. **Stockage haché** SHA256
|
|
4. **Dépendance `_require_pro_user`** pour vérification JWT + tier
|
|
|
|
### Patterns Établis à Réutiliser
|
|
|
|
```python
|
|
# Pattern authentification et vérification tier
|
|
@router.delete("/{key_id}")
|
|
async def revoke_api_key(
|
|
key_id: str,
|
|
user=Depends(_require_pro_user),
|
|
):
|
|
if not user:
|
|
return JSONResponse(status_code=401, content={"error": "UNAUTHORIZED", ...})
|
|
|
|
tier = getattr(user, "tier", None) or (
|
|
"pro" if user.plan.value in ("pro", "business", "enterprise") else "free"
|
|
)
|
|
|
|
if tier != "pro":
|
|
return JSONResponse(status_code=403, content={"error": "PRO_FEATURE_REQUIRED", ...})
|
|
|
|
# Logique de révocation...
|
|
```
|
|
|
|
### Points d'Attention Identifiés
|
|
|
|
1. **Ne pas utiliser `HTTPException`** - Utiliser `JSONResponse` pour format structuré
|
|
2. **Toujours snake_case** dans les réponses JSON
|
|
3. **Vérifier `is_active`** dans l'authentification API (middleware)
|
|
|
|
## Intelligence Git (Commits Récents)
|
|
|
|
Derniers commits analysés:
|
|
- `3d37ce4`: PostgreSQL database infrastructure
|
|
- `c4d6cae`: Redis sessions, security hardening
|
|
- `dfd45d9`: Admin login endpoint
|
|
|
|
**Patterns identifiés**:
|
|
- Utilisation de `JSONResponse` pour les réponses structurées
|
|
- Tests avec `pytest` et fixtures dans `conftest.py`
|
|
- Session DB avec `get_sync_session()` context manager
|
|
|
|
## Contexte Métier
|
|
|
|
### Epic 3: API & Automation (Pro)
|
|
|
|
Cette story est la **deuxième de l'Epic 3** qui permet aux utilisateurs Pro (Thomas) d'automatiser les traductions via:
|
|
1. ~~API Keys - Génération~~ (Story 3.1 ✅)
|
|
2. **API Keys - Révocation** (cette story)
|
|
3. Authentification X-API-Key (Story 3.4)
|
|
4. Webhooks (Stories 3.7-3.8)
|
|
5. Glossaires (Stories 3.9-3.10)
|
|
6. Custom Prompts (Stories 3.11-3.12)
|
|
|
|
### Valeur Business
|
|
|
|
La révocation est critique pour:
|
|
- Sécuriser un compte si la clé est compromise
|
|
- Gérer la rotation des clés
|
|
- Contrôler l'accès automatisé
|
|
|
|
### Dépendances
|
|
|
|
- **Story 3.1** (prérequis): Génération de clés API ✅
|
|
- **Story 3.4** (impact): L'authentification API doit vérifier `is_active`
|
|
|
|
## Guardrails Développeur
|
|
|
|
### ❌ À NE PAS FAIRE
|
|
|
|
1. **NE PAS** supprimer physiquement la clé de la DB (soft delete avec `is_active=False`)
|
|
2. **NE PAS** permettre la révocation d'une clé d'un autre utilisateur
|
|
3. **NE PAS** utiliser `HTTPException` avec `detail` string (utiliser JSONResponse structuré)
|
|
4. **NE PAS** oublier la vérification du tier Pro (403 pour Free)
|
|
5. **NE PAS** utiliser camelCase dans les réponses JSON (toujours snake_case)
|
|
|
|
### ✅ À FAIRE
|
|
|
|
1. **TOUJOURS** filtrer par `user_id` dans la query (sécurité propriété)
|
|
2. **TOUJOURS** utiliser soft delete (`is_active=False`)
|
|
3. **TOUJOURS** retourner 404 si clé non trouvée (pas 403 pour éviter énumération)
|
|
4. **TOUJOURS** suivre le format de réponse `{data: {...}, meta: {...}}`
|
|
5. **TOUJOURS** écrire des tests pour tous les cas (succès, erreur, edge cases)
|
|
6. **VÉRIFIER** que l'authentification API (Story 3.4) vérifie `is_active`
|
|
|
|
## Code Suggéré
|
|
|
|
### Endpoint DELETE à ajouter dans `routes/api_key_routes.py`
|
|
|
|
```python
|
|
from datetime import datetime, timezone
|
|
|
|
@router.delete("/{key_id}")
|
|
async def revoke_api_key(
|
|
key_id: str,
|
|
user=Depends(_require_pro_user),
|
|
):
|
|
"""
|
|
Revoke an API key.
|
|
|
|
Returns:
|
|
200: API key revoked successfully
|
|
401: Authentication required
|
|
403: Pro subscription required
|
|
404: API key not found
|
|
"""
|
|
if not user:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"error": "UNAUTHORIZED",
|
|
"message": "Authentification requise",
|
|
},
|
|
)
|
|
|
|
tier = getattr(user, "tier", None) or (
|
|
"pro" if user.plan.value in ("pro", "business", "enterprise") else "free"
|
|
)
|
|
|
|
if tier != "pro":
|
|
return JSONResponse(
|
|
status_code=403,
|
|
content={
|
|
"error": "PRO_FEATURE_REQUIRED",
|
|
"message": "Cette fonctionnalite necessite un abonnement Pro",
|
|
},
|
|
)
|
|
|
|
with get_sync_session() as session:
|
|
# ⭐ Sécurité: filtrer par user_id pour que seul le propriétaire puisse révoquer
|
|
api_key = (
|
|
session.query(ApiKey)
|
|
.filter(
|
|
ApiKey.id == key_id,
|
|
ApiKey.user_id == user.id
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not api_key:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={
|
|
"error": "API_KEY_NOT_FOUND",
|
|
"message": "Clé API non trouvée ou n'appartient pas à l'utilisateur",
|
|
},
|
|
)
|
|
|
|
# Soft delete - marquer comme inactive
|
|
api_key.is_active = False
|
|
session.commit()
|
|
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": {
|
|
"id": api_key.id,
|
|
"revoked": True,
|
|
"revoked_at": datetime.now(timezone.utc).isoformat(),
|
|
},
|
|
"meta": {},
|
|
},
|
|
)
|
|
```
|
|
|
|
### Vérification Auth API (pour Story 3.4)
|
|
|
|
L'authentification par clé API doit vérifier `is_active`:
|
|
|
|
```python
|
|
# Dans le middleware/fonction d'auth API
|
|
def verify_api_key(api_key: str) -> User:
|
|
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
|
|
with get_sync_session() as session:
|
|
key_record = session.query(ApiKey).filter(
|
|
ApiKey.key_hash == key_hash,
|
|
ApiKey.is_active == True # ⭐ Vérifier que la clé est active
|
|
).first()
|
|
|
|
if not key_record:
|
|
raise InvalidAPIKeyError("API_KEY_REVOKED" if ... else "INVALID_API_KEY")
|
|
```
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude 3.5 Sonnet (claude-3-5-sonnet)
|
|
|
|
### Debug Log References
|
|
|
|
Aucun problème rencontré lors de l'implémentation.
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ Analyse exhaustive du contexte terminée - guide complet créé pour le développeur
|
|
- ✅ Code existant analysé (routes/api_key_routes.py, database/models.py)
|
|
- ✅ Patterns de la Story 3.1 réutilisables identifiés
|
|
- ✅ Impact sur Story 3.4 (auth API) documenté
|
|
- ✅ Endpoint DELETE implémenté dans routes/api_key_routes.py
|
|
- ✅ Fonction get_user_by_api_key ajoutée dans services/auth_service.py avec vérification is_active
|
|
- ✅ Gestion des erreurs API_KEY_REVOKED ajoutée dans routes/translate_routes.py
|
|
- ✅ 22 tests créés couvrant tous les AC
|
|
|
|
### Code Review Fixes Applied (2026-02-22)
|
|
|
|
Les issues suivantes ont été corrigées après la code review:
|
|
|
|
1. **HIGH - Champ `revoked_at` non persisté**: Ajout du champ `revoked_at` au modèle `ApiKey` dans `database/models.py`
|
|
2. **HIGH - Double révocation retourne 200**: Ajout du filtre `is_active=True` dans la query - une clé déjà révoquée retourne maintenant 404
|
|
3. **HIGH - UUID validation**: Ajout de la validation du format UUID pour `key_id` - retourne 400 avec `INVALID_KEY_ID` si invalide
|
|
4. **MEDIUM - Logging**: Ajout du logging pour les révocations (audit de sécurité)
|
|
5. **Migration DB**: Création de `alembic/versions/003_add_revoked_at_to_api_keys.py`
|
|
6. **Tests**: Mise à jour des tests pour refléter les corrections (double révocation = 404, validation UUID)
|
|
|
|
### File List
|
|
|
|
- `routes/api_key_routes.py` - MODIFIÉ - Ajout endpoint DELETE /{key_id} avec validation UUID et logging
|
|
- `services/auth_service.py` - MODIFIÉ - Ajout fonction get_user_by_api_key avec vérification is_active
|
|
- `routes/translate_routes.py` - MODIFIÉ - Gestion erreurs API_KEY_REVOKED et API_KEY_EXPIRED
|
|
- `database/models.py` - MODIFIÉ - Ajout champ revoked_at au modèle ApiKey
|
|
- `alembic/versions/003_add_revoked_at_to_api_keys.py` - CRÉÉ - Migration pour colonne revoked_at
|
|
- `tests/test_story_3_2_api_key_revocation.py` - MODIFIÉ - 22 tests couvrant tous les AC + edge cases
|
|
|
|
## Change Log
|
|
|
|
- 2026-02-22: Story créée avec contexte complet (patterns Story 3.1, code suggéré)
|
|
- 2026-02-22: Implémentation terminée - Endpoint DELETE, auth API vérification, tests
|
|
|
|
## Checklist de Validation
|
|
|
|
Avant de marquer cette story comme terminée, vérifier:
|
|
|
|
- [x] `DELETE /api/v1/api-keys/{key_id}` retourne 200 avec confirmation
|
|
- [x] La clé est marquée `is_active=False` (soft delete)
|
|
- [x] Un utilisateur ne peut pas révoquer la clé d'un autre utilisateur (404)
|
|
- [x] Les utilisateurs Free reçoivent 403 avec `PRO_FEATURE_REQUIRED`
|
|
- [x] Les utilisateurs non authentifiés reçoivent 401
|
|
- [x] Tous les tests passent (18/18)
|
|
- [x] L'authentification API vérifie `is_active` (implémenté dans get_user_by_api_key)
|