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>
294 lines
11 KiB
Markdown
294 lines
11 KiB
Markdown
# Story 3.3: Admin - Révocation API Key (Any User)
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
En tant qu'**Admin**,
|
|
Je veux **révoquer la clé API de n'importe quel utilisateur**,
|
|
de sorte que **je puisse gérer la sécurité et prévenir les abus**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Endpoint Admin DELETE**: `DELETE /api/v1/admin/api-keys/{key_id}` révoque la clé API spécifiée. (FR31)
|
|
2. **Accès Admin Requis**: Seul un administrateur authentifié peut accéder à cet endpoint.
|
|
3. **Révocation Universelle**: L'admin peut révoquer N'IMPORTE quelle clé, peu importe le propriétaire.
|
|
4. **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`.
|
|
5. **Réponse Confirmée**: Retourne 200 avec confirmation de révocation dans le format `{data: {...}, meta: {}}`.
|
|
6. **Audit Logging**: L'action est journalisée avec `admin_id` et `reason` (paramètre optionnel).
|
|
7. **Clé Introuvable**: Si la clé n'existe pas, retourne 404 avec `API_KEY_NOT_FOUND`.
|
|
8. **Paramètre Raison**: L'admin peut optionnellement fournir une raison pour la révocation (body parameter).
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Implémenter l'Endpoint Admin DELETE** (AC: #1, #2, #3, #4, #5, #8)
|
|
- [x] 1.1 Ajouter la route `DELETE /api/v1/admin/api-keys/{key_id}` dans `main.py`
|
|
- [x] 1.2 Utiliser la dépendance `require_admin` existante pour l'authentification
|
|
- [x] 1.3 Ajouter paramètre optionnel `reason` dans le body de la requête
|
|
- [x] 1.4 Rechercher la clé par `id` uniquement (PAS de filtre user_id)
|
|
- [x] 1.5 Si trouvée, définir `is_active=False` et sauvegarder
|
|
- [x] 1.6 Retourner 200 avec confirmation `{data: {id, revoked: true, revoked_at, reason?}, meta: {}}`
|
|
|
|
- [x] **Task 2: Implémenter l'Audit Logging** (AC: #6)
|
|
- [x] 2.1 Logger l'action avec structlog ou logging standard
|
|
- [x] 2.2 Inclure: timestamp, admin identifier, key_id, key owner user_id, reason
|
|
- [x] 2.3 Niveau de log: INFO pour révocation réussie
|
|
|
|
- [x] **Task 3: Gérer les Cas d'Erreur** (AC: #7)
|
|
- [x] 3.1 Retourner 404 si clé non trouvée
|
|
- [x] 3.2 Retourner 401 si non authentifié admin (déjà géré par require_admin)
|
|
- [x] 3.3 Vérifier que la clé existe avant de tenter la révocation
|
|
|
|
- [x] **Task 4: Ajouter les Tests** (AC: Tous)
|
|
- [x] 4.1 Test révocation réussie par admin
|
|
- [x] 4.2 Test révocation avec raison fournie
|
|
- [x] 4.3 Test révocation échoue sans authentification admin (401)
|
|
- [x] 4.4 Test révocation de clé inexistante (404)
|
|
- [x] 4.5 Test clé révoquée ne peut plus authentifier (vérifier via get_user_by_api_key)
|
|
- [x] 4.6 Test vérifier que l'audit logging fonctionne
|
|
|
|
## Dev Notes
|
|
|
|
### Infrastructure Existante (Ne pas réimplémenter)
|
|
|
|
**Fonction Admin Auth** (`main.py` lignes 215-236):
|
|
```python
|
|
async def require_admin(authorization: Optional[str] = Header(None)) -> bool:
|
|
"""Dependency to require admin authentication"""
|
|
# Vérifie le token admin (stocké en mémoire ou via JWT)
|
|
# Retourne True si valide, sinon lève HTTPException
|
|
```
|
|
|
|
**Modèle ApiKey** (`database/models.py`):
|
|
```python
|
|
class ApiKey(Base):
|
|
__tablename__ = "api_keys"
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(String(36), ForeignKey("users.id")) # Propriétaire de la clé
|
|
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
|
|
# ... autres champs
|
|
```
|
|
|
|
**Pattern Admin Routes** (`main.py`):
|
|
- Toutes les routes admin sont directement dans `main.py` avec préfixe `/api/v1/admin/`
|
|
- Utilisent `is_admin: bool = Depends(require_admin)` comme dépendance
|
|
|
|
### Différences Critiques avec Story 3.2
|
|
|
|
| Aspect | Story 3.2 (User) | Story 3.3 (Admin) |
|
|
|--------|------------------|-------------------|
|
|
| Endpoint | `DELETE /api/v1/api-keys/{key_id}` | `DELETE /api/v1/admin/api-keys/{key_id}` |
|
|
| Auth | `_require_pro_user` (Pro tier) | `require_admin` (Admin) |
|
|
| Filtre Query | `user_id == user.id` (propriété) | Aucun (accès universel) |
|
|
| Logging | Non requis | **Obligatoire** avec admin_id + reason |
|
|
| Raison | Non applicable | Paramètre optionnel |
|
|
|
|
### Patterns à Suivre
|
|
|
|
**Format de Réponse Succès**:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "abc123",
|
|
"revoked": true,
|
|
"revoked_at": "2024-01-15T10:30:00Z",
|
|
"owner_user_id": "user-uuid-here",
|
|
"reason": "Violation des conditions d'utilisation"
|
|
},
|
|
"meta": {}
|
|
}
|
|
```
|
|
|
|
**Format de Réponse Erreur**:
|
|
```json
|
|
{
|
|
"error": "API_KEY_NOT_FOUND",
|
|
"message": "Clé API non trouvée"
|
|
}
|
|
```
|
|
|
|
**Pattern de Query Admin** (SANS filtre user_id):
|
|
```python
|
|
# Admin peut révoquer N'IMPORTE quelle clé
|
|
api_key = session.query(ApiKey).filter(
|
|
ApiKey.id == key_id
|
|
).first()
|
|
```
|
|
|
|
### Structure de Fichiers
|
|
|
|
```
|
|
main.py # MODIFIER - Ajouter DELETE /api/v1/admin/api-keys/{key_id}
|
|
routes/api_key_routes.py # Existant - Story 3.2 (ne pas modifier)
|
|
services/auth_service.py # Existant - get_user_by_api_key vérifie déjà is_active
|
|
database/models.py # Existant - ApiKey model
|
|
tests/test_story_3_3_admin_api_key_revocation.py # CRÉER
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
- Les routes admin sont centralisées dans `main.py` (pas de fichier routes/admin_routes.py)
|
|
- Pattern établi par les autres endpoints admin existants
|
|
- Le logging utilise `structlog` ou le module `logging` standard de Python
|
|
|
|
### Références
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.3]
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#Admin Dashboard]
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
|
|
- [Source: main.py#require_admin (lignes 215-236)]
|
|
- [Source: routes/api_key_routes.py#revoke_api_key (lignes 257-308)]
|
|
|
|
## Intelligence de la Story Précédente (3.2)
|
|
|
|
### Ce qui a été implémenté
|
|
|
|
1. **Endpoint DELETE** pour révocation utilisateur à `DELETE /api/v1/api-keys/{key_id}`
|
|
2. **Soft delete** avec `is_active=False`
|
|
3. **Fonction `get_user_by_api_key`** dans `services/auth_service.py` vérifie `is_active`
|
|
4. **18 tests** complets dans `tests/test_story_3_2_api_key_revocation.py`
|
|
|
|
### Patterns Établis à Réutiliser
|
|
|
|
```python
|
|
from datetime import datetime, timezone
|
|
from fastapi.responses import JSONResponse
|
|
|
|
# Soft delete pattern
|
|
api_key.is_active = False
|
|
session.commit()
|
|
|
|
# Response format
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": {
|
|
"id": api_key.id,
|
|
"revoked": True,
|
|
"revoked_at": datetime.now(timezone.utc).isoformat(),
|
|
},
|
|
"meta": {},
|
|
},
|
|
)
|
|
```
|
|
|
|
### Points d'Attention Identifiés
|
|
|
|
1. **NE PAS** utiliser `HTTPException` avec `detail` string - Utiliser `JSONResponse` structuré
|
|
2. **TOUJOURS** snake_case dans les réponses JSON
|
|
3. **VÉRIFIER** que `get_user_by_api_key` vérifie déjà `is_active` (✅ confirmé)
|
|
|
|
## Intelligence Git (Commits Récents)
|
|
|
|
Derniers commits pertinents:
|
|
- `dfd45d9`: Admin login endpoint - patterns d'authentification admin
|
|
- `80318a8`: Admin dashboard - structure des routes admin
|
|
|
|
**Patterns identifiés**:
|
|
- Routes admin directement dans `main.py`
|
|
- Dépendance `require_admin` pour l'auth
|
|
- Tests avec `pytest` dans `tests/`
|
|
|
|
## Contexte Métier
|
|
|
|
### Epic 3: API & Automation (Pro)
|
|
|
|
Cette story est la **troisième de l'Epic 3** qui permet aux utilisateurs Pro d'automatiser les traductions:
|
|
|
|
1. ~~API Keys - Génération~~ (Story 3.1 ✅)
|
|
2. ~~API Keys - Révocation User~~ (Story 3.2 ✅)
|
|
3. **API Keys - Révocation Admin** (cette story)
|
|
4. Authentification X-API-Key (Story 3.4 - backlog)
|
|
5. API Versioning (Story 3.5 - backlog)
|
|
6. Documentation OpenAPI (Story 3.6 - backlog)
|
|
7. Webhooks (Stories 3.7-3.8 - backlog)
|
|
8. Glossaires (Stories 3.9-3.10 - backlog)
|
|
9. Custom Prompts (Stories 3.11-3.12 - backlog)
|
|
|
|
### Valeur Business
|
|
|
|
La révocation admin est critique pour:
|
|
- Gérer les abus (surutilisation, comportement suspect)
|
|
- Répondre aux demandes de support (compte compromis)
|
|
- Appliquer les conditions d'utilisation
|
|
- Contrôle de sécurité centralisé
|
|
|
|
### Dépendances
|
|
|
|
- **Story 3.1** (prérequis): Génération de clés API ✅
|
|
- **Story 3.2** (prérequis): Révocation utilisateur ✅
|
|
- **Story 3.4** (impact): L'authentification API vérifie `is_active` ✅ (déjà implémenté)
|
|
|
|
## 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** filtrer par `user_id` dans la query (admin a accès universel)
|
|
3. **NE PAS** utiliser `HTTPException` avec `detail` string (utiliser JSONResponse structuré)
|
|
4. **NE PAS** oublier l'audit logging (obligatoire pour cette story)
|
|
5. **NE PAS** utiliser camelCase dans les réponses JSON (toujours snake_case)
|
|
6. **NE PAS** créer un fichier `routes/admin_routes.py` - les routes admin restent dans `main.py`
|
|
|
|
### ✅ À FAIRE
|
|
|
|
1. **TOUJOURS** utiliser `require_admin` dependency pour l'authentification
|
|
2. **TOUJOURS** soft delete (`is_active=False`)
|
|
3. **TOUJOURS** logger l'action avec admin_id, key_id, owner_user_id, et reason
|
|
4. **TOUJOURS** retourner 404 si clé non trouvée
|
|
5. **TOUJOURS** suivre le format de réponse `{data: {...}, meta: {...}}`
|
|
6. **TOUJOURS** écrire des tests pour tous les cas
|
|
7. **INCLURE** le `owner_user_id` dans la réponse pour traçabilité
|
|
|
|
## 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
|
|
- ✅ Patterns de la Story 3.2 réutilisables identifiés
|
|
- ✅ Code existant analysé (main.py, routes/api_key_routes.py)
|
|
- ✅ Différences critiques avec Story 3.2 documentées
|
|
- ✅ Endpoint DELETE /api/v1/admin/api-keys/{key_id} implémenté dans main.py
|
|
- ✅ AdminRevokeApiKeyRequest model ajouté pour le body parameter optionnel `reason`
|
|
- ✅ Soft delete avec is_active=False et revoked_at timestamp
|
|
- ✅ Audit logging avec logger.info incluant admin_id, key_id, owner_user_id, reason, timestamp
|
|
- ✅ 15 tests créés et tous passent (tests/test_story_3_3_admin_api_key_revocation.py)
|
|
- ✅ Migration alembic 003_add_revoked_at_to_api_keys.py appliquée
|
|
- ✅ Tests de régression passent (Story 3.2 et admin tier change)
|
|
|
|
### File List
|
|
|
|
- `main.py` - MODIFIÉ - Ajout endpoint DELETE /api/v1/admin/api-keys/{key_id}, AdminRevokeApiKeyRequest model, require_admin retourne admin_id
|
|
- `tests/test_story_3_3_admin_api_key_revocation.py` - CRÉÉ - 15 tests couvrant tous les AC
|
|
|
|
## Change Log
|
|
|
|
- 2026-02-22: Story créée avec contexte complet (patterns Story 3.2, code suggéré, différences critiques)
|
|
- 2026-02-22: Implémentation terminée - Endpoint DELETE, audit logging, tests
|
|
- 2026-02-22: Code review - Fix dead code (lignes dupliquées supprimées), fix AC6 (admin_id ajouté au log), fix tests (vérification admin_id ajoutée)
|
|
|
|
## Checklist de Validation
|
|
|
|
Avant de marquer cette story comme terminée, vérifier:
|
|
|
|
- [x] `DELETE /api/v1/admin/api-keys/{key_id}` retourne 200 avec confirmation
|
|
- [x] La clé est marquée `is_active=False` (soft delete)
|
|
- [x] L'admin peut révoquer n'importe quelle clé (pas de filtre user_id)
|
|
- [x] L'action est journalisée avec admin_id, key_id, owner_user_id, reason
|
|
- [x] Le paramètre `reason` optionnel fonctionne correctement
|
|
- [x] Les utilisateurs non-admin reçoivent 401
|
|
- [x] Les clés inexistantes retournent 404
|
|
- [x] Tous les tests passent (15/15)
|
|
- [x] La clé révoquée ne peut plus authentifier (vérifier via test)
|