baobab-auth-api 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. baobab_auth_api/__init__.py +13 -0
  2. baobab_auth_api/app.py +79 -0
  3. baobab_auth_api/config.py +113 -0
  4. baobab_auth_api/error_handlers.py +123 -0
  5. baobab_auth_api/exceptions/__init__.py +26 -0
  6. baobab_auth_api/exceptions/auth.py +71 -0
  7. baobab_auth_api/exceptions/registration.py +29 -0
  8. baobab_auth_api/integration/__init__.py +13 -0
  9. baobab_auth_api/integration/core_endpoint_spec.py +20 -0
  10. baobab_auth_api/integration/core_exception_http_mapper.py +57 -0
  11. baobab_auth_api/integration/core_integration_gaps.py +26 -0
  12. baobab_auth_api/integration/core_route_catalog.py +83 -0
  13. baobab_auth_api/lifespan.py +35 -0
  14. baobab_auth_api/log_filters.py +53 -0
  15. baobab_auth_api/main.py +12 -0
  16. baobab_auth_api/models/__init__.py +22 -0
  17. baobab_auth_api/models/audit_event.py +38 -0
  18. baobab_auth_api/models/audit_event_type.py +38 -0
  19. baobab_auth_api/models/permission.py +27 -0
  20. baobab_auth_api/models/role.py +30 -0
  21. baobab_auth_api/models/session.py +35 -0
  22. baobab_auth_api/models/token_pair.py +27 -0
  23. baobab_auth_api/models/user.py +38 -0
  24. baobab_auth_api/openapi.py +37 -0
  25. baobab_auth_api/ports/__init__.py +18 -0
  26. baobab_auth_api/ports/audit_repository.py +25 -0
  27. baobab_auth_api/ports/jwt_service.py +62 -0
  28. baobab_auth_api/ports/password_service.py +34 -0
  29. baobab_auth_api/ports/session_repository.py +72 -0
  30. baobab_auth_api/ports/user_repository.py +64 -0
  31. baobab_auth_api/py.typed +0 -0
  32. baobab_auth_api/routers/__init__.py +5 -0
  33. baobab_auth_api/routers/auth.py +218 -0
  34. baobab_auth_api/routers/health.py +61 -0
  35. baobab_auth_api/routers/jwks.py +47 -0
  36. baobab_auth_api/routers/permissions.py +50 -0
  37. baobab_auth_api/routers/roles.py +51 -0
  38. baobab_auth_api/schemas/__init__.py +35 -0
  39. baobab_auth_api/schemas/auth.py +64 -0
  40. baobab_auth_api/schemas/errors.py +39 -0
  41. baobab_auth_api/schemas/jwks.py +23 -0
  42. baobab_auth_api/schemas/permission_list_response.py +21 -0
  43. baobab_auth_api/schemas/permissions.py +28 -0
  44. baobab_auth_api/schemas/role_list_response.py +21 -0
  45. baobab_auth_api/schemas/roles.py +31 -0
  46. baobab_auth_api/schemas/tokens.py +56 -0
  47. baobab_auth_api/schemas/users.py +33 -0
  48. baobab_auth_api/services/__init__.py +20 -0
  49. baobab_auth_api/services/audit_service.py +53 -0
  50. baobab_auth_api/services/auth_service.py +198 -0
  51. baobab_auth_api/services/permission_service.py +108 -0
  52. baobab_auth_api/services/role_service.py +83 -0
  53. baobab_auth_api/services/session_service.py +114 -0
  54. baobab_auth_api/services/user_service.py +117 -0
  55. baobab_auth_api-0.9.0.dist-info/METADATA +276 -0
  56. baobab_auth_api-0.9.0.dist-info/RECORD +58 -0
  57. baobab_auth_api-0.9.0.dist-info/WHEEL +4 -0
  58. baobab_auth_api-0.9.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,13 @@
1
+ """Package baobab_auth_api — API FastAPI d'authentification pour l'écosystème Baobab.
2
+
3
+ Expose la factory ``create_app()`` et la classe de configuration ``AuthApiSettings``
4
+ comme contrat public du package.
5
+
6
+ :spec: CDC § 21.2
7
+ :origin: docs/specifications/cahier-des-charges/cahier_des_charges.md
8
+ """
9
+
10
+ from baobab_auth_api.app import AppFactory
11
+ from baobab_auth_api.config import AuthApiSettings
12
+
13
+ __all__: list[str] = ["AppFactory", "AuthApiSettings"]
baobab_auth_api/app.py ADDED
@@ -0,0 +1,79 @@
1
+ """Factory de l'application FastAPI baobab-auth-api.
2
+
3
+ :spec: FEAT-002.2 FEAT-008.1
4
+ :origin: docs/specifications/us/US-002-configuration/FEAT-002.2-create-app.rst
5
+ """
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
10
+
11
+ from baobab_auth_api.config import AuthApiSettings
12
+ from baobab_auth_api.error_handlers import ErrorHandlerRegistry
13
+ from baobab_auth_api.lifespan import Lifespan
14
+ from baobab_auth_api.openapi import OpenApiMetadata
15
+ from baobab_auth_api.routers.health import HealthRouter
16
+
17
+
18
+ class AppFactory:
19
+ """Factory statique qui instancie et configure l'application FastAPI.
20
+
21
+ Compose ``OpenApiMetadata``, ``Lifespan``, ``ErrorHandlerRegistry``
22
+ et les routers. Accepte un ``settings`` optionnel pour l'injection
23
+ en tests.
24
+
25
+ :spec: FEAT-002.2
26
+ """
27
+
28
+ @staticmethod
29
+ def create(settings: AuthApiSettings | None = None) -> FastAPI:
30
+ """Crée et retourne une instance FastAPI complètement configurée.
31
+
32
+ :param settings: Configuration injectée. Si ``None``, charge
33
+ ``AuthApiSettings()`` depuis les variables d'environnement.
34
+ :returns: Instance ``FastAPI`` prête à être servie.
35
+ """
36
+ if settings is None:
37
+ settings = AuthApiSettings() # type: ignore[call-arg]
38
+
39
+ meta = OpenApiMetadata()
40
+
41
+ app = FastAPI(
42
+ title=meta.title,
43
+ version=meta.version,
44
+ description=meta.description,
45
+ contact=meta.contact,
46
+ license_info=meta.license_info,
47
+ lifespan=Lifespan.handler,
48
+ )
49
+
50
+ if settings.cors_origins:
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=settings.cors_origins,
54
+ allow_credentials=True,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+
59
+ if settings.trusted_hosts:
60
+ app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.trusted_hosts)
61
+
62
+ ErrorHandlerRegistry.register(app)
63
+
64
+ AppFactory._include_routers(app)
65
+
66
+ return app
67
+
68
+ @staticmethod
69
+ def _include_routers(app: FastAPI) -> None:
70
+ """Inclut les routers de l'application.
71
+
72
+ :param app: Instance FastAPI sur laquelle inclure les routers.
73
+ """
74
+ from fastapi import APIRouter
75
+
76
+ router_auth = APIRouter(prefix="/auth", tags=["auth"])
77
+ app.include_router(router_auth)
78
+
79
+ app.include_router(HealthRouter.create())
@@ -0,0 +1,113 @@
1
+ """Configuration injectée de l'API d'authentification Baobab.
2
+
3
+ :spec: FEAT-002.1 FEAT-009.1
4
+ :origin: docs/specifications/us/US-002-configuration/FEAT-002.1-auth-api-settings.rst
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ from pydantic import field_validator, model_validator
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+ _SUPPORTED_JWT_ALGORITHMS: frozenset[str] = frozenset(
13
+ {"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
14
+ )
15
+
16
+
17
+ class AuthApiSettings(BaseSettings):
18
+ """Configuration de l'API FastAPI d'authentification Baobab.
19
+
20
+ Toutes les valeurs sont injectées par variables d'environnement
21
+ préfixées ``BAOBAB_AUTH_`` ou par fichier ``.env``.
22
+ Aucun secret ne figure dans le code source.
23
+
24
+ :spec: FEAT-002.1
25
+ """
26
+
27
+ model_config = SettingsConfigDict(
28
+ env_prefix="BAOBAB_AUTH_",
29
+ env_file=".env",
30
+ env_file_encoding="utf-8",
31
+ extra="ignore",
32
+ )
33
+
34
+ database_url: str
35
+ issuer: str = "baobab-auth"
36
+ audience: str = "baobab-api"
37
+ access_token_ttl_seconds: int = 900
38
+ refresh_token_ttl_seconds: int = 2_592_000
39
+ jwt_algorithm: str = "RS256"
40
+ password_min_length: int = 12
41
+ log_level: str = "INFO"
42
+ cors_origins: list[str] = []
43
+ trusted_hosts: list[str] = []
44
+ """Liste des hôtes autorisés pour TrustedHostMiddleware (vide = désactivé)."""
45
+
46
+ environment: Literal["development", "staging", "production"] = "production"
47
+
48
+ @field_validator("database_url", mode="after")
49
+ @classmethod
50
+ def database_url_non_vide(cls, v: str) -> str:
51
+ """Vérifie que l'URL de base de données n'est pas vide.
52
+
53
+ :param v: Valeur du champ ``database_url``.
54
+ :returns: La valeur inchangée si valide.
55
+ :raises ValueError: Si l'URL est vide ou ne contient que des espaces.
56
+ :spec: FEAT-002.1 critère 2
57
+ """
58
+ if not v.strip():
59
+ raise ValueError("database_url ne peut pas être vide")
60
+ return v
61
+
62
+ @field_validator("access_token_ttl_seconds", "refresh_token_ttl_seconds", mode="after")
63
+ @classmethod
64
+ def ttl_strictement_positif(cls, v: int) -> int:
65
+ """Vérifie que les TTL sont strictement positifs.
66
+
67
+ :param v: Valeur du TTL en secondes.
68
+ :returns: La valeur inchangée si valide.
69
+ :raises ValueError: Si le TTL est inférieur ou égal à zéro.
70
+ :spec: FEAT-002.1 critère 3
71
+ """
72
+ if v <= 0:
73
+ raise ValueError("Le TTL doit être strictement positif")
74
+ return v
75
+
76
+ @field_validator("jwt_algorithm", mode="after")
77
+ @classmethod
78
+ def algo_jwt_supporte(cls, v: str) -> str:
79
+ """Vérifie que l'algorithme JWT est dans la liste blanche asymétrique.
80
+
81
+ Seuls les algorithmes asymétriques (RSA, EC) sont autorisés.
82
+ Les algorithmes HMAC (HS256, etc.) sont refusés.
83
+
84
+ :param v: Nom de l'algorithme JWT.
85
+ :returns: La valeur inchangée si valide.
86
+ :raises ValueError: Si l'algorithme n'est pas supporté.
87
+ :spec: FEAT-002.1 critère 4
88
+ """
89
+ if v not in _SUPPORTED_JWT_ALGORITHMS:
90
+ raise ValueError(
91
+ f"Algorithme JWT non supporté : '{v}'. "
92
+ f"Algorithmes autorisés : {sorted(_SUPPORTED_JWT_ALGORITHMS)}"
93
+ )
94
+ return v
95
+
96
+ @model_validator(mode="after")
97
+ def cors_wildcard_production_interdit(self) -> "AuthApiSettings":
98
+ """Refuse le CORS wildcard en environnement de production.
99
+
100
+ Un wildcard ``"*"`` dans ``cors_origins`` est acceptable en développement
101
+ mais constitue une faille de sécurité en production.
102
+
103
+ :returns: L'instance validée.
104
+ :raises ValueError: Si ``"*"`` est présent dans ``cors_origins``
105
+ et que l'environnement est ``"production"``.
106
+ :spec: FEAT-002.1 critère 5
107
+ """
108
+ if self.environment == "production" and "*" in self.cors_origins:
109
+ raise ValueError(
110
+ "CORS wildcard ('*') interdit en environnement 'production'. "
111
+ "Définir des origines explicites dans BAOBAB_AUTH_CORS_ORIGINS."
112
+ )
113
+ return self
@@ -0,0 +1,123 @@
1
+ """Handlers d'erreurs HTTP pour l'application FastAPI baobab-auth-api.
2
+
3
+ :spec: FEAT-002.2
4
+ :origin: docs/specifications/us/US-002-configuration/FEAT-002.2-create-app.rst
5
+ """
6
+
7
+ import logging
8
+
9
+ from baobab_auth_core.exceptions.base import BaobabAuthCoreError
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from fastapi.exceptions import RequestValidationError
12
+ from fastapi.responses import JSONResponse
13
+
14
+ from baobab_auth_api.integration.core_exception_http_mapper import CoreExceptionHttpMapper
15
+
16
+ logger = logging.getLogger(__name__)
17
+ _CORE_MAPPER = CoreExceptionHttpMapper()
18
+
19
+
20
+ class ErrorHandlerRegistry:
21
+ """Enregistreur centralisé des handlers d'erreurs HTTP sur FastAPI.
22
+
23
+ Expose une méthode de classe ``register`` qui branche tous les handlers
24
+ sur l'instance FastAPI fournie. Aucun secret ni stack trace ne transite
25
+ dans les réponses d'erreur.
26
+
27
+ :spec: FEAT-002.2
28
+ """
29
+
30
+ @classmethod
31
+ def register(cls, app: FastAPI) -> None:
32
+ """Enregistre tous les handlers d'erreurs sur l'application FastAPI.
33
+
34
+ :param app: Instance FastAPI sur laquelle brancher les handlers.
35
+ :returns: None
36
+ """
37
+ app.add_exception_handler(
38
+ RequestValidationError,
39
+ cls._handle_validation_error, # type: ignore[arg-type]
40
+ )
41
+ app.add_exception_handler(
42
+ HTTPException,
43
+ cls._handle_http_exception, # type: ignore[arg-type]
44
+ )
45
+ app.add_exception_handler(
46
+ BaobabAuthCoreError,
47
+ cls._handle_core_exception, # type: ignore[arg-type]
48
+ )
49
+ app.add_exception_handler(
50
+ Exception,
51
+ cls._handle_generic_exception,
52
+ )
53
+
54
+ @staticmethod
55
+ async def _handle_validation_error(
56
+ request: Request, exc: RequestValidationError
57
+ ) -> JSONResponse:
58
+ """Handler pour les erreurs de validation Pydantic (422).
59
+
60
+ :param request: Requête HTTP entrante.
61
+ :param exc: Exception de validation levée.
62
+ :returns: Réponse JSON structurée avec le détail des erreurs.
63
+ """
64
+ return JSONResponse(
65
+ status_code=422,
66
+ content={
67
+ "error": "validation_error",
68
+ "detail": exc.errors(),
69
+ },
70
+ )
71
+
72
+ @staticmethod
73
+ async def _handle_http_exception(request: Request, exc: HTTPException) -> JSONResponse:
74
+ """Handler pour les HTTPException FastAPI.
75
+
76
+ :param request: Requête HTTP entrante.
77
+ :param exc: HTTPException levée.
78
+ :returns: Réponse JSON uniforme.
79
+ """
80
+ return JSONResponse(
81
+ status_code=exc.status_code,
82
+ content={
83
+ "error": "http_error",
84
+ "detail": exc.detail,
85
+ },
86
+ )
87
+
88
+ @staticmethod
89
+ async def _handle_core_exception(
90
+ request: Request,
91
+ exc: BaobabAuthCoreError,
92
+ ) -> JSONResponse:
93
+ """Handler pour les exceptions métier ``baobab-auth-core``.
94
+
95
+ :param request: Requête HTTP entrante.
96
+ :param exc: Exception core levée par un use case.
97
+ :returns: Réponse JSON ``{"error": {"code", "message"}}``.
98
+ """
99
+ _ = request
100
+ status, code, message = _CORE_MAPPER.resolve(exc)
101
+ return JSONResponse(
102
+ status_code=status,
103
+ content={"error": {"code": code, "message": message}},
104
+ )
105
+
106
+ @staticmethod
107
+ async def _handle_generic_exception(request: Request, exc: Exception) -> JSONResponse:
108
+ """Handler fallback pour toute exception non anticipée (500).
109
+
110
+ Ne retourne aucune information interne pour éviter les fuites.
111
+
112
+ :param request: Requête HTTP entrante.
113
+ :param exc: Exception non gérée.
114
+ :returns: Réponse JSON générique 500.
115
+ """
116
+ logger.exception("Erreur interne non anticipée : %s", type(exc).__name__)
117
+ return JSONResponse(
118
+ status_code=500,
119
+ content={
120
+ "error": "internal_server_error",
121
+ "detail": "Une erreur interne s'est produite.",
122
+ },
123
+ )
@@ -0,0 +1,26 @@
1
+ """Exceptions métier de baobab-auth-api.
2
+
3
+ :spec: FEAT-004
4
+ """
5
+
6
+ from baobab_auth_api.exceptions.auth import (
7
+ AccountDisabledError,
8
+ ExpiredTokenError,
9
+ InvalidCredentialsError,
10
+ InvalidTokenError,
11
+ SessionRevokedError,
12
+ )
13
+ from baobab_auth_api.exceptions.registration import (
14
+ EmailAlreadyExistsError,
15
+ WeakPasswordError,
16
+ )
17
+
18
+ __all__ = [
19
+ "AccountDisabledError",
20
+ "EmailAlreadyExistsError",
21
+ "ExpiredTokenError",
22
+ "InvalidCredentialsError",
23
+ "InvalidTokenError",
24
+ "SessionRevokedError",
25
+ "WeakPasswordError",
26
+ ]
@@ -0,0 +1,71 @@
1
+ """Exceptions d'authentification et de session.
2
+
3
+ :spec: FEAT-004.1 FEAT-004.2
4
+ :origin: docs/specifications/us/US-004-services/FEAT-004.1-auth-service.rst
5
+ """
6
+
7
+ from uuid import UUID
8
+
9
+
10
+ class InvalidCredentialsError(Exception):
11
+ """Credentials invalides (email inconnu ou mot de passe erroné).
12
+
13
+ Le message d'erreur HTTP ne doit pas distinguer les deux cas
14
+ afin d'éviter l'énumération des comptes.
15
+
16
+ :spec: FEAT-004.1
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ """:spec: FEAT-004.1."""
21
+ super().__init__("Invalid credentials")
22
+
23
+
24
+ class AccountDisabledError(Exception):
25
+ """Le compte utilisateur est désactivé.
26
+
27
+ :spec: FEAT-004.2
28
+ """
29
+
30
+ def __init__(self, user_id: UUID) -> None:
31
+ """:param user_id: UUID du compte désactivé. :spec: FEAT-004.2"""
32
+ super().__init__(f"Account {user_id} is disabled")
33
+ self.user_id = user_id
34
+
35
+
36
+ class SessionRevokedError(Exception):
37
+ """La session a été révoquée (ou le refresh token réutilisé après rotation).
38
+
39
+ :spec: FEAT-004.2
40
+ """
41
+
42
+ def __init__(self, session_id: UUID | None = None) -> None:
43
+ """:param session_id: UUID de la session révoquée. :spec: FEAT-004.2"""
44
+ msg = f"Session {session_id} is revoked" if session_id else "Session is revoked"
45
+ super().__init__(msg)
46
+ self.session_id = session_id
47
+
48
+
49
+ class InvalidTokenError(Exception):
50
+ """Token JWT malformé ou signature invalide.
51
+
52
+ :spec: FEAT-004.1
53
+ """
54
+
55
+ def __init__(self, detail: str = "Invalid token") -> None:
56
+ """:param detail: Message décrivant l'invalidité du token. :spec: FEAT-004.1"""
57
+ super().__init__(detail)
58
+
59
+
60
+ class ExpiredTokenError(InvalidTokenError):
61
+ """Token JWT expiré.
62
+
63
+ Sous-classe de :exc:`InvalidTokenError` pour permettre une capture
64
+ générique ou spécialisée.
65
+
66
+ :spec: FEAT-004.1
67
+ """
68
+
69
+ def __init__(self) -> None:
70
+ """:spec: FEAT-004.1."""
71
+ super().__init__("Token has expired")
@@ -0,0 +1,29 @@
1
+ """Exceptions liées à l'inscription d'un nouvel utilisateur.
2
+
3
+ :spec: FEAT-004.1 FEAT-004.2
4
+ :origin: docs/specifications/us/US-004-services/FEAT-004.1-auth-service.rst
5
+ """
6
+
7
+
8
+ class WeakPasswordError(ValueError):
9
+ """Mot de passe trop court ou ne respectant pas la politique de sécurité.
10
+
11
+ :spec: FEAT-004.2
12
+ """
13
+
14
+ def __init__(self, min_length: int) -> None:
15
+ """:param min_length: Longueur minimale requise. :spec: FEAT-004.2"""
16
+ super().__init__(f"Password must be at least {min_length} characters long")
17
+ self.min_length = min_length
18
+
19
+
20
+ class EmailAlreadyExistsError(ValueError):
21
+ """Adresse e-mail déjà utilisée par un compte existant.
22
+
23
+ :spec: FEAT-004.1
24
+ """
25
+
26
+ def __init__(self, email: str) -> None:
27
+ """:param email: Adresse e-mail en conflit. :spec: FEAT-004.1"""
28
+ super().__init__(f"Email '{email}' is already registered")
29
+ self.email = email
@@ -0,0 +1,13 @@
1
+ """Intégration avec ``baobab-auth-core`` (contrats, mapping HTTP)."""
2
+
3
+ from baobab_auth_api.integration.core_endpoint_spec import CoreEndpointSpec
4
+ from baobab_auth_api.integration.core_exception_http_mapper import CoreExceptionHttpMapper
5
+ from baobab_auth_api.integration.core_integration_gaps import CoreIntegrationGaps
6
+ from baobab_auth_api.integration.core_route_catalog import CoreRouteCatalog
7
+
8
+ __all__ = [
9
+ "CoreEndpointSpec",
10
+ "CoreExceptionHttpMapper",
11
+ "CoreIntegrationGaps",
12
+ "CoreRouteCatalog",
13
+ ]
@@ -0,0 +1,20 @@
1
+ """Spécification d'un endpoint HTTP aligné sur le contrat core."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class CoreEndpointSpec:
8
+ """Route HTTP attendue pour un cas d'usage ``baobab-auth-core``.
9
+
10
+ :param method: verbe HTTP (``GET``, ``POST``, …).
11
+ :param path: chemin relatif (ex. ``/auth/login``).
12
+ :param use_case: nom du use case core cible.
13
+ :param command: commande core associée ; ``None`` pour les lectures.
14
+ :spec: FEAT-011.1
15
+ """
16
+
17
+ method: str
18
+ path: str
19
+ use_case: str
20
+ command: str | None = None
@@ -0,0 +1,57 @@
1
+ """Mapping exceptions ``baobab-auth-core`` → réponses HTTP."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from baobab_auth_core.exceptions.base import BaobabAuthCoreError
6
+
7
+
8
+ class CoreExceptionHttpMapper:
9
+ """Convertit une exception métier core en statut HTTP et payload d'erreur.
10
+
11
+ S'appuie uniquement sur ``error_code`` et ``safe_message`` — jamais sur le
12
+ message interne de l'exception.
13
+
14
+ :spec: FEAT-011.1
15
+ """
16
+
17
+ _CODE_TO_HTTP: ClassVar[dict[str, int]] = {
18
+ "auth.validation.invalid_email": 422,
19
+ "auth.validation.weak_password": 422, # nosec B105
20
+ "auth.validation.invalid_role_name": 422,
21
+ "auth.user.already_exists": 409,
22
+ "auth.user.not_found": 404,
23
+ "auth.user.disabled": 403,
24
+ "auth.user.locked": 423,
25
+ "auth.user.deleted": 403,
26
+ "auth.user.account_disabled": 403,
27
+ "auth.user.account_pending": 403,
28
+ "auth.credentials.invalid": 401,
29
+ "auth.authorization.unauthorized": 401,
30
+ "auth.token.invalid": 401,
31
+ "auth.token.expired": 401,
32
+ "auth.session.expired": 401,
33
+ "auth.session.revoked": 401,
34
+ "auth.session.not_found": 404,
35
+ "auth.authorization.forbidden": 403,
36
+ "auth.authorization.permission_denied": 403,
37
+ "auth.role.not_found": 404,
38
+ "auth.permission.not_found": 404,
39
+ "auth.role.last_super_admin": 409,
40
+ }
41
+
42
+ def resolve(self, exc: BaobabAuthCoreError) -> tuple[int, str, str]:
43
+ """Résout le statut HTTP et le corps d'erreur pour une exception core.
44
+
45
+ :param exc: exception métier levée par un use case core.
46
+ :returns: tuple ``(status_code, error_code, safe_message)``.
47
+ """
48
+ status = self._CODE_TO_HTTP.get(exc.error_code, 400)
49
+ return status, exc.error_code, exc.safe_message
50
+
51
+ @classmethod
52
+ def covered_codes(cls) -> frozenset[str]:
53
+ """Renvoie l'ensemble des ``error_code`` mappés explicitement.
54
+
55
+ :returns: codes couverts par le mapping HTTP.
56
+ """
57
+ return frozenset(cls._CODE_TO_HTTP)
@@ -0,0 +1,26 @@
1
+ """Écarts documentés entre l'API et ``baobab-auth-core`` 0.9.0."""
2
+
3
+
4
+ class CoreIntegrationGaps:
5
+ """Liste les écarts connus d'intégration avec ``baobab-auth-core``.
6
+
7
+ :spec: FEAT-011.1
8
+ """
9
+
10
+ GAPS: tuple[str, ...] = (
11
+ "Les services v0.1.0 (``AuthService``, modèles locaux) n'orchestrent pas encore "
12
+ "les use cases exportés du core — migration progressive prévue.",
13
+ "Huit routes contractuelles §9.1 manquantes (sessions admin, rôles, disable/enable, "
14
+ "rotation JWK) — voir ``CoreRouteCatalog.missing_from_contract()``.",
15
+ "``AppFactory`` n'injecte pas encore database/security ; ``baobab-auth-database`` "
16
+ "reporté en extra ``[database]`` jusqu'à alignement 0.9.x.",
17
+ "Validation E2E inter-briques → prérequis ``1.0.0``.",
18
+ )
19
+
20
+ @classmethod
21
+ def documented_gaps(cls) -> tuple[str, ...]:
22
+ """Renvoie les écarts documentés pour revue PO/architecte.
23
+
24
+ :returns: tuple de descriptions d'écarts.
25
+ """
26
+ return cls.GAPS
@@ -0,0 +1,83 @@
1
+ """Catalogue des routes HTTP — contrat core vs implémentation actuelle."""
2
+
3
+ from baobab_auth_api.integration.core_endpoint_spec import CoreEndpointSpec
4
+
5
+
6
+ class CoreRouteCatalog:
7
+ """Référentiel des endpoints ``api_contract.md`` et de l'implémentation v0.1.0.
8
+
9
+ :spec: FEAT-011.1
10
+ """
11
+
12
+ CONTRACT: tuple[CoreEndpointSpec, ...] = (
13
+ CoreEndpointSpec("POST", "/auth/register", "RegisterUser", "RegisterUserCommand"),
14
+ CoreEndpointSpec("POST", "/auth/login", "AuthenticateUser", "AuthenticateUserCommand"),
15
+ CoreEndpointSpec("POST", "/auth/refresh", "RefreshSession", "RefreshSessionCommand"),
16
+ CoreEndpointSpec("POST", "/auth/logout", "Logout", "LogoutCommand"),
17
+ CoreEndpointSpec("GET", "/auth/me", "GetCurrentUser"),
18
+ CoreEndpointSpec("GET", "/auth/sessions", "ListUserSessions"),
19
+ CoreEndpointSpec(
20
+ "POST",
21
+ "/auth/sessions/{id}/revoke",
22
+ "RevokeSession",
23
+ "RevokeSessionCommand",
24
+ ),
25
+ CoreEndpointSpec(
26
+ "POST",
27
+ "/auth/users/{id}/sessions/revoke",
28
+ "RevokeAllSessions",
29
+ "RevokeAllSessionsCommand",
30
+ ),
31
+ CoreEndpointSpec("GET", "/auth/roles", "ListRoles"),
32
+ CoreEndpointSpec("GET", "/auth/permissions", "ListPermissions"),
33
+ CoreEndpointSpec("POST", "/auth/users/{id}/roles", "AssignRole", "AssignRoleCommand"),
34
+ CoreEndpointSpec(
35
+ "DELETE",
36
+ "/auth/users/{id}/roles/{role}",
37
+ "RemoveRole",
38
+ "RemoveRoleCommand",
39
+ ),
40
+ CoreEndpointSpec("POST", "/auth/users/{id}/disable", "DisableUser", "DisableUserCommand"),
41
+ CoreEndpointSpec("POST", "/auth/users/{id}/enable", "EnableUser", "EnableUserCommand"),
42
+ CoreEndpointSpec(
43
+ "POST",
44
+ "/auth/jwks/rotation-request",
45
+ "RequestJwkRotation",
46
+ "RequestJwkRotationCommand",
47
+ ),
48
+ )
49
+
50
+ IMPLEMENTED: tuple[CoreEndpointSpec, ...] = (
51
+ CoreEndpointSpec("POST", "/auth/register", "RegisterUser", "RegisterUserCommand"),
52
+ CoreEndpointSpec("POST", "/auth/login", "AuthenticateUser", "AuthenticateUserCommand"),
53
+ CoreEndpointSpec("POST", "/auth/refresh", "RefreshSession", "RefreshSessionCommand"),
54
+ CoreEndpointSpec("POST", "/auth/logout", "Logout", "LogoutCommand"),
55
+ CoreEndpointSpec("GET", "/auth/me", "GetCurrentUser"),
56
+ CoreEndpointSpec("GET", "/auth/roles", "ListRoles"),
57
+ CoreEndpointSpec("GET", "/auth/permissions", "ListPermissions"),
58
+ )
59
+
60
+ @classmethod
61
+ def contract_keys(cls) -> frozenset[tuple[str, str]]:
62
+ """Renvoie les couples (méthode, chemin) du contrat core.
63
+
64
+ :returns: ensemble immuable de clés de route.
65
+ """
66
+ return frozenset((spec.method, spec.path) for spec in cls.CONTRACT)
67
+
68
+ @classmethod
69
+ def implemented_keys(cls) -> frozenset[tuple[str, str]]:
70
+ """Renvoie les couples (méthode, chemin) implémentés en v0.1.0.
71
+
72
+ :returns: ensemble immuable de clés de route.
73
+ """
74
+ return frozenset((spec.method, spec.path) for spec in cls.IMPLEMENTED)
75
+
76
+ @classmethod
77
+ def missing_from_contract(cls) -> tuple[CoreEndpointSpec, ...]:
78
+ """Liste les routes contractuelles non encore exposées.
79
+
80
+ :returns: endpoints manquants par rapport au contrat core.
81
+ """
82
+ implemented = cls.implemented_keys()
83
+ return tuple(spec for spec in cls.CONTRACT if (spec.method, spec.path) not in implemented)