baobab-auth-database 0.1.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 (63) hide show
  1. baobab_auth_database/__init__.py +46 -0
  2. baobab_auth_database/auth_base.py +20 -0
  3. baobab_auth_database/auth_engine_factory.py +48 -0
  4. baobab_auth_database/auth_session_factory.py +27 -0
  5. baobab_auth_database/auth_sql_types.py +42 -0
  6. baobab_auth_database/bootstrap/__init__.py +12 -0
  7. baobab_auth_database/bootstrap/default_auth_catalog.py +98 -0
  8. baobab_auth_database/bootstrap/seed_defaults.py +150 -0
  9. baobab_auth_database/cli/__init__.py +12 -0
  10. baobab_auth_database/cli/auth_database_cli.py +123 -0
  11. baobab_auth_database/cli/cli_configuration.py +37 -0
  12. baobab_auth_database/cli/main.py +16 -0
  13. baobab_auth_database/database_url_masker.py +27 -0
  14. baobab_auth_database/exceptions/__init__.py +22 -0
  15. baobab_auth_database/exceptions/database_errors.py +46 -0
  16. baobab_auth_database/mappers/__init__.py +20 -0
  17. baobab_auth_database/mappers/audit_event_mapper.py +89 -0
  18. baobab_auth_database/mappers/permission_mapper.py +69 -0
  19. baobab_auth_database/mappers/role_mapper.py +83 -0
  20. baobab_auth_database/mappers/session_mapper.py +92 -0
  21. baobab_auth_database/mappers/user_mapper.py +96 -0
  22. baobab_auth_database/mappers/user_profile_mapper.py +80 -0
  23. baobab_auth_database/migration_runner.py +146 -0
  24. baobab_auth_database/migrations/__init__.py +12 -0
  25. baobab_auth_database/migrations/env.py +11 -0
  26. baobab_auth_database/migrations/migration_environment.py +107 -0
  27. baobab_auth_database/migrations/migration_resource_locator.py +59 -0
  28. baobab_auth_database/migrations/script.py.mako +28 -0
  29. baobab_auth_database/migrations/versions/0001_initial_auth_schema.py +178 -0
  30. baobab_auth_database/migrations/versions/0002_align_core_schema.py +54 -0
  31. baobab_auth_database/migrations/versions/__init__.py +4 -0
  32. baobab_auth_database/models/__init__.py +26 -0
  33. baobab_auth_database/models/audit_event_model.py +37 -0
  34. baobab_auth_database/models/jwk_key_model.py +52 -0
  35. baobab_auth_database/models/permission_model.py +44 -0
  36. baobab_auth_database/models/profile_model.py +46 -0
  37. baobab_auth_database/models/role_model.py +48 -0
  38. baobab_auth_database/models/role_permission_model.py +45 -0
  39. baobab_auth_database/models/session_model.py +69 -0
  40. baobab_auth_database/models/user_model.py +73 -0
  41. baobab_auth_database/models/user_role_model.py +45 -0
  42. baobab_auth_database/naming_convention.py +26 -0
  43. baobab_auth_database/py.typed +0 -0
  44. baobab_auth_database/repositories/__init__.py +32 -0
  45. baobab_auth_database/repositories/jwk_key_record.py +32 -0
  46. baobab_auth_database/repositories/repository_support.py +67 -0
  47. baobab_auth_database/repositories/sql_alchemy_audit_repository.py +73 -0
  48. baobab_auth_database/repositories/sql_alchemy_auth_unit_of_work.py +142 -0
  49. baobab_auth_database/repositories/sql_alchemy_jwk_key_repository.py +112 -0
  50. baobab_auth_database/repositories/sql_alchemy_permission_repository.py +117 -0
  51. baobab_auth_database/repositories/sql_alchemy_role_repository.py +125 -0
  52. baobab_auth_database/repositories/sql_alchemy_session_repository.py +125 -0
  53. baobab_auth_database/repositories/sql_alchemy_user_repository.py +151 -0
  54. baobab_auth_database/settings.py +86 -0
  55. baobab_auth_database/testing/__init__.py +14 -0
  56. baobab_auth_database/testing/auth_schema_assertions.py +64 -0
  57. baobab_auth_database/testing/auth_sqlite_test_helper.py +72 -0
  58. baobab_auth_database/testing/auth_test_factories.py +202 -0
  59. baobab_auth_database-0.1.0.dist-info/METADATA +342 -0
  60. baobab_auth_database-0.1.0.dist-info/RECORD +63 -0
  61. baobab_auth_database-0.1.0.dist-info/WHEEL +4 -0
  62. baobab_auth_database-0.1.0.dist-info/entry_points.txt +2 -0
  63. baobab_auth_database-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,46 @@
1
+ """Adaptateur de persistance de l'écosystème ``baobab-auth``.
2
+
3
+ Le package expose l'API publique de la librairie ``baobab-auth-database``.
4
+ Les symboles listés dans ``__all__`` à la racine et dans chaque sous-package
5
+ constituent le contrat SemVer consommable par les projets parents.
6
+
7
+ Sous-packages publics stables : ``bootstrap``, ``cli``, ``mappers``,
8
+ ``migrations``, ``models``, ``repositories``, ``testing``.
9
+
10
+ :spec: FEAT-001.1, FEAT-002.1, FEAT-002.2, FEAT-002.3, FEAT-003.1, FEAT-004.3,
11
+ FEAT-008.3
12
+ """
13
+
14
+ from baobab_auth_database.auth_base import AuthBase
15
+ from baobab_auth_database.auth_engine_factory import AuthEngineFactory
16
+ from baobab_auth_database.auth_session_factory import AuthSessionFactory
17
+ from baobab_auth_database.auth_sql_types import AuthSqlTypes
18
+ from baobab_auth_database.database_url_masker import DatabaseUrlMasker
19
+ from baobab_auth_database.exceptions import (
20
+ AuthDatabaseConfigurationError,
21
+ AuthDatabaseConnectionError,
22
+ AuthDatabaseError,
23
+ AuthDatabaseMappingError,
24
+ AuthDatabaseMigrationError,
25
+ AuthDatabaseOperationError,
26
+ )
27
+ from baobab_auth_database.migration_runner import MigrationRunner
28
+ from baobab_auth_database.naming_convention import NamingConvention
29
+ from baobab_auth_database.settings import AuthDatabaseSettings
30
+
31
+ __all__: tuple[str, ...] = (
32
+ "AuthBase",
33
+ "AuthDatabaseConfigurationError",
34
+ "AuthDatabaseConnectionError",
35
+ "AuthDatabaseError",
36
+ "AuthDatabaseMappingError",
37
+ "AuthDatabaseMigrationError",
38
+ "AuthDatabaseOperationError",
39
+ "AuthDatabaseSettings",
40
+ "AuthEngineFactory",
41
+ "AuthSessionFactory",
42
+ "AuthSqlTypes",
43
+ "DatabaseUrlMasker",
44
+ "MigrationRunner",
45
+ "NamingConvention",
46
+ )
@@ -0,0 +1,20 @@
1
+ """Base déclarative SQLAlchemy.
2
+
3
+ :spec: FEAT-003.1
4
+ """
5
+
6
+ from typing import ClassVar
7
+
8
+ from sqlalchemy import MetaData
9
+ from sqlalchemy.orm import DeclarativeBase
10
+
11
+ from baobab_auth_database.naming_convention import NamingConvention
12
+
13
+
14
+ class AuthBase(DeclarativeBase):
15
+ """Base déclarative commune des futurs modèles ORM auth.
16
+
17
+ :spec: FEAT-003.1
18
+ """
19
+
20
+ metadata: ClassVar[MetaData] = MetaData(naming_convention=NamingConvention.as_dict())
@@ -0,0 +1,48 @@
1
+ """Factory de création d'engine SQLAlchemy.
2
+
3
+ :spec: FEAT-002.2, FEAT-002.3
4
+ """
5
+
6
+ from sqlalchemy import create_engine
7
+ from sqlalchemy.engine import Engine
8
+ from sqlalchemy.engine.url import make_url
9
+ from sqlalchemy.exc import SQLAlchemyError
10
+
11
+ from baobab_auth_database.database_url_masker import DatabaseUrlMasker
12
+ from baobab_auth_database.exceptions import AuthDatabaseConnectionError
13
+ from baobab_auth_database.settings import AuthDatabaseSettings
14
+
15
+
16
+ class AuthEngineFactory:
17
+ """Factory d'engine SQLAlchemy pour la persistance auth.
18
+
19
+ :spec: FEAT-002.2, FEAT-002.3
20
+ """
21
+
22
+ @classmethod
23
+ def create_auth_engine(
24
+ cls: type["AuthEngineFactory"], settings: AuthDatabaseSettings
25
+ ) -> Engine:
26
+ """Créer une engine SQLAlchemy depuis les settings database.
27
+
28
+ :param settings: Configuration database validée.
29
+ :returns: Engine SQLAlchemy configurée pour la librairie auth.
30
+ :raises AuthDatabaseConnectionError: Si SQLAlchemy rejette l'URL ou les options.
31
+ :spec: FEAT-002.2, FEAT-002.3
32
+ """
33
+ try:
34
+ database_url = make_url(settings.database_url)
35
+ engine_options: dict[str, object] = {
36
+ "echo": settings.database_echo,
37
+ "pool_pre_ping": settings.database_pool_pre_ping,
38
+ }
39
+
40
+ if database_url.get_backend_name() != "sqlite":
41
+ engine_options["pool_size"] = settings.database_pool_size
42
+ engine_options["max_overflow"] = settings.database_max_overflow
43
+
44
+ return create_engine(database_url, **engine_options)
45
+ except SQLAlchemyError as exc:
46
+ masked_url = DatabaseUrlMasker.mask_url(settings.database_url)
47
+ msg = f"Unable to create auth database engine for {masked_url}."
48
+ raise AuthDatabaseConnectionError(msg) from exc
@@ -0,0 +1,27 @@
1
+ """Factory de création de sessions SQLAlchemy.
2
+
3
+ :spec: FEAT-002.2
4
+ """
5
+
6
+ from sqlalchemy.engine import Engine
7
+ from sqlalchemy.orm import Session, sessionmaker
8
+
9
+
10
+ class AuthSessionFactory:
11
+ """Factory de sessionmaker SQLAlchemy pour la persistance auth.
12
+
13
+ :spec: FEAT-002.2
14
+ """
15
+
16
+ @classmethod
17
+ def create_auth_session_factory(
18
+ cls: type["AuthSessionFactory"],
19
+ engine: Engine,
20
+ ) -> sessionmaker[Session]:
21
+ """Créer un ``sessionmaker`` sans commit implicite.
22
+
23
+ :param engine: Engine SQLAlchemy à lier aux sessions.
24
+ :returns: Factory de sessions SQLAlchemy.
25
+ :spec: FEAT-002.2
26
+ """
27
+ return sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
@@ -0,0 +1,42 @@
1
+ """Types SQL communs de la couche auth.
2
+
3
+ :spec: FEAT-003.1
4
+ """
5
+
6
+ from uuid import UUID
7
+
8
+ from sqlalchemy.types import JSON, DateTime, TypeEngine, Uuid
9
+
10
+
11
+ class AuthSqlTypes:
12
+ """Factory de types SQLAlchemy compatibles SQLite et PostgreSQL.
13
+
14
+ :spec: FEAT-003.1
15
+ """
16
+
17
+ @classmethod
18
+ def uuid(cls: type["AuthSqlTypes"]) -> TypeEngine[UUID]:
19
+ """Créer un type UUID SQLAlchemy.
20
+
21
+ :returns: Type SQLAlchemy UUID avec valeurs Python ``uuid.UUID``.
22
+ :spec: FEAT-003.1
23
+ """
24
+ return Uuid(as_uuid=True)
25
+
26
+ @classmethod
27
+ def timestamp(cls: type["AuthSqlTypes"]) -> DateTime:
28
+ """Créer un type timestamp timezone-aware.
29
+
30
+ :returns: Type SQLAlchemy ``DateTime`` avec timezone activée.
31
+ :spec: FEAT-003.1
32
+ """
33
+ return DateTime(timezone=True)
34
+
35
+ @classmethod
36
+ def json(cls: type["AuthSqlTypes"]) -> JSON:
37
+ """Créer un type JSON SQLAlchemy générique.
38
+
39
+ :returns: Type SQLAlchemy JSON compatible SQLite/PostgreSQL.
40
+ :spec: FEAT-003.1
41
+ """
42
+ return JSON()
@@ -0,0 +1,12 @@
1
+ """Bootstrap idempotent rôles et permissions système.
2
+
3
+ :spec: FEAT-007.1
4
+ """
5
+
6
+ from baobab_auth_database.bootstrap.default_auth_catalog import DefaultAuthCatalog
7
+ from baobab_auth_database.bootstrap.seed_defaults import SeedDefaults
8
+
9
+ __all__: tuple[str, ...] = (
10
+ "DefaultAuthCatalog",
11
+ "SeedDefaults",
12
+ )
@@ -0,0 +1,98 @@
1
+ """Catalogue normatif des rôles et permissions système.
2
+
3
+ :spec: FEAT-007.1
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import ClassVar
9
+
10
+
11
+ class DefaultAuthCatalog:
12
+ """Constantes et matrice rôle-permission du bootstrap auth.
13
+
14
+ :spec: FEAT-007.1
15
+ """
16
+
17
+ DEFAULT_ROLES: ClassVar[tuple[str, ...]] = (
18
+ "USER",
19
+ "ADMIN",
20
+ "SUPER_ADMIN",
21
+ )
22
+
23
+ DEFAULT_PERMISSIONS: ClassVar[tuple[str, ...]] = (
24
+ "auth:user:read",
25
+ "auth:user:write",
26
+ "auth:role:read",
27
+ "auth:role:write",
28
+ "auth:permission:read",
29
+ "auth:permission:write",
30
+ "auth:session:read",
31
+ "auth:session:revoke",
32
+ "auth:audit:read",
33
+ "auth:jwk:read",
34
+ "auth:jwk:write",
35
+ )
36
+
37
+ DEFAULT_ROLE_PERMISSIONS: ClassVar[dict[str, tuple[str, ...]]] = {
38
+ "USER": (
39
+ "auth:user:read",
40
+ "auth:session:read",
41
+ ),
42
+ "ADMIN": (
43
+ "auth:user:read",
44
+ "auth:user:write",
45
+ "auth:role:read",
46
+ "auth:permission:read",
47
+ "auth:session:read",
48
+ "auth:session:revoke",
49
+ "auth:audit:read",
50
+ "auth:jwk:read",
51
+ ),
52
+ "SUPER_ADMIN": DEFAULT_PERMISSIONS,
53
+ }
54
+
55
+ DEFAULT_ROLE_DESCRIPTIONS: ClassVar[dict[str, str]] = {
56
+ "USER": "Utilisateur standard : consultation de son compte et de ses sessions.",
57
+ "ADMIN": "Administration opérationnelle des utilisateurs, sessions, audit et JWKS.",
58
+ "SUPER_ADMIN": "Administration complète du système d'authentification.",
59
+ }
60
+
61
+ DEFAULT_PERMISSION_DESCRIPTIONS: ClassVar[dict[str, str]] = {
62
+ "auth:user:read": "Lire un utilisateur (périmètre restreint au compte courant pour USER).",
63
+ "auth:user:write": "Créer, modifier ou gérer un utilisateur et ses rôles.",
64
+ "auth:role:read": "Lire les rôles disponibles et leurs associations.",
65
+ "auth:role:write": "Créer, modifier ou supprimer un rôle système ou applicatif.",
66
+ "auth:permission:read": "Lire les permissions disponibles.",
67
+ "auth:permission:write": "Créer, modifier ou supprimer une permission.",
68
+ "auth:session:read": "Lire les sessions (périmètre restreint pour USER).",
69
+ "auth:session:revoke": "Révoquer une session.",
70
+ "auth:audit:read": "Consulter les événements d'audit.",
71
+ "auth:jwk:read": "Inspecter les clés JWKS en administration.",
72
+ "auth:jwk:write": "Créer, importer, désactiver ou préparer la rotation de clés JWKS.",
73
+ }
74
+
75
+ @classmethod
76
+ def validate_catalog(cls: type[DefaultAuthCatalog]) -> None:
77
+ """Vérifier la cohérence interne du catalogue.
78
+
79
+ :returns: ``None``.
80
+ :raises ValueError: Si un rôle ou une permission référencé est absent du catalogue.
81
+ :spec: FEAT-007.1
82
+ """
83
+ role_names = set(cls.DEFAULT_ROLES)
84
+ permission_codes = set(cls.DEFAULT_PERMISSIONS)
85
+ for role_name, permission_codes_for_role in cls.DEFAULT_ROLE_PERMISSIONS.items():
86
+ if role_name not in role_names:
87
+ message = f"Rôle inconnu dans DEFAULT_ROLE_PERMISSIONS : {role_name}."
88
+ raise ValueError(message)
89
+ for permission_code in permission_codes_for_role:
90
+ if permission_code not in permission_codes:
91
+ message = (
92
+ f"Permission {permission_code} référencée pour {role_name} "
93
+ "mais absente de DEFAULT_PERMISSIONS."
94
+ )
95
+ raise ValueError(message)
96
+ if set(cls.DEFAULT_ROLE_PERMISSIONS) != role_names:
97
+ message = "DEFAULT_ROLE_PERMISSIONS doit couvrir exactement DEFAULT_ROLES."
98
+ raise ValueError(message)
@@ -0,0 +1,150 @@
1
+ """Bootstrap idempotent des rôles et permissions système.
2
+
3
+ :spec: FEAT-007.1
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import UTC, datetime
9
+ from uuid import uuid4
10
+
11
+ from baobab_auth_core import Permission, Role
12
+ from baobab_auth_core.domain.value_objects.permission_name import PermissionName
13
+ from baobab_auth_core.domain.value_objects.role_name import RoleName
14
+ from baobab_auth_core.domain.value_objects.utc_datetime import UtcDatetime
15
+
16
+ from baobab_auth_database.bootstrap.default_auth_catalog import DefaultAuthCatalog
17
+ from baobab_auth_database.repositories.sql_alchemy_auth_unit_of_work import SqlAlchemyAuthUnitOfWork
18
+
19
+
20
+ class SeedDefaults:
21
+ """Seeds idempotents des rôles et permissions par défaut.
22
+
23
+ :spec: FEAT-007.1
24
+ """
25
+
26
+ @classmethod
27
+ def seed_default_roles_and_permissions(
28
+ cls: type[SeedDefaults],
29
+ unit_of_work: SqlAlchemyAuthUnitOfWork,
30
+ ) -> None:
31
+ """Créer ou compléter les rôles, permissions et associations système.
32
+
33
+ Le seed ne supprime ni ne remplace les entités inconnues ajoutées par une
34
+ application consommatrice. Les associations existantes sont préservées et
35
+ complétées selon la matrice normative.
36
+
37
+ :param unit_of_work: Unit of Work ouverte (``__enter__`` déjà appelé).
38
+ :returns: ``None``.
39
+ :spec: FEAT-007.1
40
+ """
41
+ DefaultAuthCatalog.validate_catalog()
42
+ cls._seed_permissions(unit_of_work)
43
+ cls._seed_roles(unit_of_work)
44
+
45
+ @classmethod
46
+ def _seed_permissions(
47
+ cls: type[SeedDefaults],
48
+ unit_of_work: SqlAlchemyAuthUnitOfWork,
49
+ ) -> None:
50
+ for permission_code in DefaultAuthCatalog.DEFAULT_PERMISSIONS:
51
+ cls._ensure_permission(unit_of_work, permission_code)
52
+
53
+ @classmethod
54
+ def _seed_roles(
55
+ cls: type[SeedDefaults],
56
+ unit_of_work: SqlAlchemyAuthUnitOfWork,
57
+ ) -> None:
58
+ for role_name in DefaultAuthCatalog.DEFAULT_ROLES:
59
+ cls._ensure_role(unit_of_work, role_name)
60
+
61
+ @classmethod
62
+ def _ensure_permission(
63
+ cls: type[SeedDefaults],
64
+ unit_of_work: SqlAlchemyAuthUnitOfWork,
65
+ permission_code: str,
66
+ ) -> None:
67
+ permission_name = PermissionName(permission_code)
68
+ existing = unit_of_work.permissions.get_by_name(permission_name)
69
+ resource, action = cls._parse_permission_code(permission_code)
70
+ description = DefaultAuthCatalog.DEFAULT_PERMISSION_DESCRIPTIONS[permission_code]
71
+ if existing is None:
72
+ unit_of_work.permissions.save(
73
+ Permission(
74
+ id=str(uuid4()),
75
+ name=permission_name,
76
+ resource=resource,
77
+ action=action,
78
+ description=description,
79
+ is_system=True,
80
+ )
81
+ )
82
+ return
83
+ if existing.description != description:
84
+ unit_of_work.permissions.save(
85
+ Permission(
86
+ id=existing.id,
87
+ name=existing.name,
88
+ resource=existing.resource,
89
+ action=existing.action,
90
+ description=description,
91
+ is_system=existing.is_system,
92
+ )
93
+ )
94
+
95
+ @classmethod
96
+ def _ensure_role(
97
+ cls: type[SeedDefaults],
98
+ unit_of_work: SqlAlchemyAuthUnitOfWork,
99
+ role_name: str,
100
+ ) -> None:
101
+ name = RoleName(role_name)
102
+ default_permission_codes = DefaultAuthCatalog.DEFAULT_ROLE_PERMISSIONS[role_name]
103
+ default_permissions = {PermissionName(code) for code in default_permission_codes}
104
+ description = DefaultAuthCatalog.DEFAULT_ROLE_DESCRIPTIONS[role_name]
105
+ existing = unit_of_work.roles.get_by_name(name)
106
+ if existing is None:
107
+ now = cls._utc_now()
108
+ unit_of_work.roles.save(
109
+ Role(
110
+ id=str(uuid4()),
111
+ name=name,
112
+ description=description,
113
+ permissions=default_permissions,
114
+ is_system=True,
115
+ created_at=now,
116
+ updated_at=now,
117
+ )
118
+ )
119
+ return
120
+ merged_permissions = existing.permissions | default_permissions
121
+ updated_description = (
122
+ description if existing.description != description else existing.description
123
+ )
124
+ if (
125
+ merged_permissions != existing.permissions
126
+ or updated_description != existing.description
127
+ ):
128
+ unit_of_work.roles.save(
129
+ Role(
130
+ id=existing.id,
131
+ name=existing.name,
132
+ description=updated_description,
133
+ permissions=merged_permissions,
134
+ is_system=existing.is_system,
135
+ created_at=existing.created_at,
136
+ updated_at=cls._utc_now(),
137
+ )
138
+ )
139
+
140
+ @staticmethod
141
+ def _parse_permission_code(permission_code: str) -> tuple[str, str]:
142
+ parts = permission_code.split(":")
143
+ if len(parts) != 3 or parts[0] != "auth":
144
+ message = f"Code de permission système invalide : {permission_code}."
145
+ raise ValueError(message)
146
+ return parts[1], parts[2]
147
+
148
+ @staticmethod
149
+ def _utc_now() -> UtcDatetime:
150
+ return UtcDatetime(datetime.now(UTC))
@@ -0,0 +1,12 @@
1
+ """Interface en ligne de commande ``baobab-auth-db``.
2
+
3
+ :spec: FEAT-007.2
4
+ """
5
+
6
+ from baobab_auth_database.cli.auth_database_cli import AuthDatabaseCli
7
+ from baobab_auth_database.cli.cli_configuration import CliConfiguration
8
+
9
+ __all__: tuple[str, ...] = (
10
+ "AuthDatabaseCli",
11
+ "CliConfiguration",
12
+ )
@@ -0,0 +1,123 @@
1
+ """Interface en ligne de commande ``baobab-auth-db``.
2
+
3
+ :spec: FEAT-007.2
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import logging
10
+ import sys
11
+ from collections.abc import Sequence
12
+
13
+ from baobab_auth_database import (
14
+ AuthEngineFactory,
15
+ AuthSessionFactory,
16
+ MigrationRunner,
17
+ )
18
+ from baobab_auth_database.bootstrap import SeedDefaults
19
+ from baobab_auth_database.cli.cli_configuration import CliConfiguration
20
+ from baobab_auth_database.repositories import SqlAlchemyAuthUnitOfWork
21
+ from baobab_auth_database.settings import AuthDatabaseSettings
22
+
23
+
24
+ class AuthDatabaseCli:
25
+ """Pilote les commandes migrations et bootstrap de la librairie.
26
+
27
+ :spec: FEAT-007.2
28
+ """
29
+
30
+ def run(
31
+ self: AuthDatabaseCli,
32
+ argv: Sequence[str] | None = None,
33
+ ) -> int:
34
+ """Exécuter la commande CLI demandée.
35
+
36
+ :param argv: Arguments (``None`` pour ``sys.argv[1:]``).
37
+ :returns: Code de sortie POSIX (``0`` si succès).
38
+ :spec: FEAT-007.2
39
+ """
40
+ parser = self._build_parser()
41
+ args = parser.parse_args(list(argv) if argv is not None else None)
42
+ if args.verbose:
43
+ logging.basicConfig(level=logging.DEBUG)
44
+ settings = CliConfiguration.load_settings(
45
+ database_url=args.database_url,
46
+ env_file=args.env_file,
47
+ )
48
+ if args.command == "upgrade":
49
+ MigrationRunner.from_settings(settings).upgrade(args.revision)
50
+ return 0
51
+ if args.command == "downgrade":
52
+ MigrationRunner.from_settings(settings).downgrade(args.revision)
53
+ return 0
54
+ if args.command == "current":
55
+ current = MigrationRunner.from_settings(settings).current()
56
+ sys.stdout.write(f"{current}\n" if current is not None else "None\n")
57
+ return 0
58
+ if args.command == "history":
59
+ history = MigrationRunner.from_settings(settings).history()
60
+ for revision in history:
61
+ sys.stdout.write(f"{revision}\n")
62
+ return 0
63
+ if args.command == "seed-defaults":
64
+ self._run_seed_defaults(settings)
65
+ return 0
66
+ parser.error(f"Commande inconnue : {args.command}")
67
+ return 2
68
+
69
+ @staticmethod
70
+ def _build_parser() -> argparse.ArgumentParser:
71
+ parser = argparse.ArgumentParser(
72
+ prog="baobab-auth-db",
73
+ description="CLI de migrations et bootstrap pour baobab-auth-database.",
74
+ )
75
+ parser.add_argument(
76
+ "--database-url",
77
+ help="URL SQLAlchemy explicite (prioritaire sur BAOBAB_AUTH_DATABASE_URL).",
78
+ )
79
+ parser.add_argument(
80
+ "--env-file",
81
+ help="Fichier d'environnement à charger avant la résolution des settings.",
82
+ )
83
+ parser.add_argument(
84
+ "--verbose",
85
+ action="store_true",
86
+ help="Active le logging de debug.",
87
+ )
88
+ subparsers = parser.add_subparsers(dest="command", required=True)
89
+
90
+ upgrade = subparsers.add_parser("upgrade", help="Appliquer les migrations.")
91
+ upgrade.add_argument(
92
+ "revision",
93
+ nargs="?",
94
+ default="head",
95
+ help="Révision cible (défaut : head).",
96
+ )
97
+
98
+ downgrade = subparsers.add_parser("downgrade", help="Revenir à une révision antérieure.")
99
+ downgrade.add_argument(
100
+ "revision",
101
+ nargs="?",
102
+ default="-1",
103
+ help="Révision cible (défaut : -1).",
104
+ )
105
+
106
+ subparsers.add_parser("current", help="Afficher la révision courante.")
107
+ subparsers.add_parser("history", help="Lister les révisions connues.")
108
+ subparsers.add_parser(
109
+ "seed-defaults",
110
+ help="Exécuter le bootstrap idempotent des rôles et permissions.",
111
+ )
112
+ return parser
113
+
114
+ @staticmethod
115
+ def _run_seed_defaults(settings: AuthDatabaseSettings) -> None:
116
+ engine = AuthEngineFactory.create_auth_engine(settings)
117
+ session_factory = AuthSessionFactory.create_auth_session_factory(engine)
118
+ try:
119
+ with SqlAlchemyAuthUnitOfWork(session_factory) as unit_of_work:
120
+ SeedDefaults.seed_default_roles_and_permissions(unit_of_work)
121
+ unit_of_work.commit()
122
+ finally:
123
+ engine.dispose()
@@ -0,0 +1,37 @@
1
+ """Résolution de configuration pour la CLI ``baobab-auth-db``.
2
+
3
+ :spec: FEAT-007.2
4
+ """
5
+
6
+ from baobab_auth_database.settings import AuthDatabaseSettings
7
+
8
+
9
+ class CliConfiguration:
10
+ """Charge les settings database pour les commandes CLI.
11
+
12
+ :spec: FEAT-007.2
13
+ """
14
+
15
+ @classmethod
16
+ def load_settings(
17
+ cls: type["CliConfiguration"],
18
+ *,
19
+ database_url: str | None = None,
20
+ env_file: str | None = None,
21
+ ) -> AuthDatabaseSettings:
22
+ """Construire les settings depuis l'environnement et les options CLI.
23
+
24
+ :param database_url: URL SQLAlchemy explicite (prioritaire sur l'environnement).
25
+ :param env_file: Fichier d'environnement optionnel à charger.
26
+ :returns: Settings validés pour la commande CLI.
27
+ :spec: FEAT-007.2
28
+ """
29
+ if env_file is not None:
30
+ settings = AuthDatabaseSettings(_env_file=env_file) # type: ignore[call-arg]
31
+ elif database_url is not None:
32
+ return AuthDatabaseSettings(database_url=database_url)
33
+ else:
34
+ settings = AuthDatabaseSettings()
35
+ if database_url is not None:
36
+ return settings.model_copy(update={"database_url": database_url})
37
+ return settings
@@ -0,0 +1,16 @@
1
+ """Point d'entrée console ``baobab-auth-db``.
2
+
3
+ :spec: FEAT-007.2
4
+ """
5
+
6
+ import sys
7
+
8
+ from baobab_auth_database.cli.auth_database_cli import AuthDatabaseCli
9
+
10
+
11
+ def main() -> None:
12
+ """Lancer la CLI et propager le code de sortie au processus.
13
+
14
+ :spec: FEAT-007.2
15
+ """
16
+ sys.exit(AuthDatabaseCli().run())
@@ -0,0 +1,27 @@
1
+ """Masquage des URLs database sensibles.
2
+
3
+ :spec: FEAT-002.3
4
+ """
5
+
6
+ from sqlalchemy.engine.url import make_url
7
+ from sqlalchemy.exc import ArgumentError
8
+
9
+
10
+ class DatabaseUrlMasker:
11
+ """Masque les informations sensibles dans une URL database.
12
+
13
+ :spec: FEAT-002.3
14
+ """
15
+
16
+ @classmethod
17
+ def mask_url(cls: type["DatabaseUrlMasker"], database_url: str) -> str:
18
+ """Rendre une URL database affichable sans mot de passe.
19
+
20
+ :param database_url: URL database brute.
21
+ :returns: URL masquée ou libellé générique si l'URL est invalide.
22
+ :spec: FEAT-002.3
23
+ """
24
+ try:
25
+ return make_url(database_url).render_as_string(hide_password=True)
26
+ except ArgumentError:
27
+ return "<invalid database url>"