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.
- baobab_auth_database/__init__.py +46 -0
- baobab_auth_database/auth_base.py +20 -0
- baobab_auth_database/auth_engine_factory.py +48 -0
- baobab_auth_database/auth_session_factory.py +27 -0
- baobab_auth_database/auth_sql_types.py +42 -0
- baobab_auth_database/bootstrap/__init__.py +12 -0
- baobab_auth_database/bootstrap/default_auth_catalog.py +98 -0
- baobab_auth_database/bootstrap/seed_defaults.py +150 -0
- baobab_auth_database/cli/__init__.py +12 -0
- baobab_auth_database/cli/auth_database_cli.py +123 -0
- baobab_auth_database/cli/cli_configuration.py +37 -0
- baobab_auth_database/cli/main.py +16 -0
- baobab_auth_database/database_url_masker.py +27 -0
- baobab_auth_database/exceptions/__init__.py +22 -0
- baobab_auth_database/exceptions/database_errors.py +46 -0
- baobab_auth_database/mappers/__init__.py +20 -0
- baobab_auth_database/mappers/audit_event_mapper.py +89 -0
- baobab_auth_database/mappers/permission_mapper.py +69 -0
- baobab_auth_database/mappers/role_mapper.py +83 -0
- baobab_auth_database/mappers/session_mapper.py +92 -0
- baobab_auth_database/mappers/user_mapper.py +96 -0
- baobab_auth_database/mappers/user_profile_mapper.py +80 -0
- baobab_auth_database/migration_runner.py +146 -0
- baobab_auth_database/migrations/__init__.py +12 -0
- baobab_auth_database/migrations/env.py +11 -0
- baobab_auth_database/migrations/migration_environment.py +107 -0
- baobab_auth_database/migrations/migration_resource_locator.py +59 -0
- baobab_auth_database/migrations/script.py.mako +28 -0
- baobab_auth_database/migrations/versions/0001_initial_auth_schema.py +178 -0
- baobab_auth_database/migrations/versions/0002_align_core_schema.py +54 -0
- baobab_auth_database/migrations/versions/__init__.py +4 -0
- baobab_auth_database/models/__init__.py +26 -0
- baobab_auth_database/models/audit_event_model.py +37 -0
- baobab_auth_database/models/jwk_key_model.py +52 -0
- baobab_auth_database/models/permission_model.py +44 -0
- baobab_auth_database/models/profile_model.py +46 -0
- baobab_auth_database/models/role_model.py +48 -0
- baobab_auth_database/models/role_permission_model.py +45 -0
- baobab_auth_database/models/session_model.py +69 -0
- baobab_auth_database/models/user_model.py +73 -0
- baobab_auth_database/models/user_role_model.py +45 -0
- baobab_auth_database/naming_convention.py +26 -0
- baobab_auth_database/py.typed +0 -0
- baobab_auth_database/repositories/__init__.py +32 -0
- baobab_auth_database/repositories/jwk_key_record.py +32 -0
- baobab_auth_database/repositories/repository_support.py +67 -0
- baobab_auth_database/repositories/sql_alchemy_audit_repository.py +73 -0
- baobab_auth_database/repositories/sql_alchemy_auth_unit_of_work.py +142 -0
- baobab_auth_database/repositories/sql_alchemy_jwk_key_repository.py +112 -0
- baobab_auth_database/repositories/sql_alchemy_permission_repository.py +117 -0
- baobab_auth_database/repositories/sql_alchemy_role_repository.py +125 -0
- baobab_auth_database/repositories/sql_alchemy_session_repository.py +125 -0
- baobab_auth_database/repositories/sql_alchemy_user_repository.py +151 -0
- baobab_auth_database/settings.py +86 -0
- baobab_auth_database/testing/__init__.py +14 -0
- baobab_auth_database/testing/auth_schema_assertions.py +64 -0
- baobab_auth_database/testing/auth_sqlite_test_helper.py +72 -0
- baobab_auth_database/testing/auth_test_factories.py +202 -0
- baobab_auth_database-0.1.0.dist-info/METADATA +342 -0
- baobab_auth_database-0.1.0.dist-info/RECORD +63 -0
- baobab_auth_database-0.1.0.dist-info/WHEEL +4 -0
- baobab_auth_database-0.1.0.dist-info/entry_points.txt +2 -0
- 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>"
|