Files
office_translator/_bmad-output/implementation-artifacts/3-2-revocation-api-key-user.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

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)