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

14 KiB

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

  • Task 1: Implémenter l'Endpoint DELETE (AC: #1, #2, #3, #4)

    • 1.1 Ajouter la route DELETE /api/v1/api-keys/{key_id} dans routes/api_key_routes.py
    • 1.2 Vérifier que l'utilisateur est authentifié et Pro
    • 1.3 Rechercher la clé par id ET user_id (sécurité propriété)
    • 1.4 Si trouvée, définir is_active=False et sauvegarder
    • 1.5 Retourner 200 avec confirmation {data: {id, revoked: true}, meta: {}}
  • Task 2: Gérer les Cas d'Erreur (AC: #5, #6, #7)

    • 2.1 Retourner 404 si clé non trouvée ou n'appartient pas à l'utilisateur
    • 2.2 Retourner 403 si utilisateur Free
    • 2.3 Retourner 401 si non authentifié
  • Task 3: Vérifier l'Impact sur l'Authentification API (AC: #3)

    • 3.1 Vérifier que le middleware d'authentification API vérifie is_active
    • 3.2 Si non vérifié, ajouter la vérification dans le middleware/fonction d'auth API
  • Task 4: Ajouter les Tests (AC: Tous)

    • 4.1 Test révocation réussie pour utilisateur Pro
    • 4.2 Test révocation échoue pour clé d'un autre utilisateur (404)
    • 4.3 Test révocation échoue pour utilisateur Free (403)
    • 4.4 Test révocation échoue sans authentification (401)
    • 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):

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):

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:

{
  "data": {
    "id": "abc123",
    "revoked": true,
    "revoked_at": "2024-01-15T10:30:00Z"
  },
  "meta": {}
}

Format de Réponse Erreur:

{
  "error": "API_KEY_NOT_FOUND",
  "message": "Clé API non trouvée ou n'appartient pas à l'utilisateur"
}

Pattern de Query avec Propriété:

# 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

# 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

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:

# 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:

  • DELETE /api/v1/api-keys/{key_id} retourne 200 avec confirmation
  • La clé est marquée is_active=False (soft delete)
  • Un utilisateur ne peut pas révoquer la clé d'un autre utilisateur (404)
  • Les utilisateurs Free reçoivent 403 avec PRO_FEATURE_REQUIRED
  • Les utilisateurs non authentifiés reçoivent 401
  • Tous les tests passent (18/18)
  • L'authentification API vérifie is_active (implémenté dans get_user_by_api_key)