baobab-auth-core 0.4.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 (91) hide show
  1. baobab_auth_core/__init__.py +111 -0
  2. baobab_auth_core/application/__init__.py +1 -0
  3. baobab_auth_core/application/commands/__init__.py +35 -0
  4. baobab_auth_core/application/commands/assign_role_command.py +22 -0
  5. baobab_auth_core/application/commands/authenticate_user_command.py +20 -0
  6. baobab_auth_core/application/commands/change_password_command.py +22 -0
  7. baobab_auth_core/application/commands/logout_command.py +20 -0
  8. baobab_auth_core/application/commands/refresh_session_command.py +20 -0
  9. baobab_auth_core/application/commands/register_user_command.py +20 -0
  10. baobab_auth_core/application/commands/remove_role_command.py +22 -0
  11. baobab_auth_core/application/commands/revoke_all_sessions_command.py +20 -0
  12. baobab_auth_core/application/commands/revoke_session_command.py +20 -0
  13. baobab_auth_core/application/results/__init__.py +17 -0
  14. baobab_auth_core/application/results/auth_context.py +40 -0
  15. baobab_auth_core/application/results/authenticate_user_result.py +22 -0
  16. baobab_auth_core/application/results/refresh_session_result.py +20 -0
  17. baobab_auth_core/application/results/register_user_result.py +16 -0
  18. baobab_auth_core/application/services/__init__.py +7 -0
  19. baobab_auth_core/application/services/authorization_service.py +56 -0
  20. baobab_auth_core/application/use_cases/__init__.py +23 -0
  21. baobab_auth_core/application/use_cases/assign_role.py +96 -0
  22. baobab_auth_core/application/use_cases/authenticate_user.py +185 -0
  23. baobab_auth_core/application/use_cases/change_password.py +99 -0
  24. baobab_auth_core/application/use_cases/logout.py +73 -0
  25. baobab_auth_core/application/use_cases/refresh_session.py +98 -0
  26. baobab_auth_core/application/use_cases/register_user.py +114 -0
  27. baobab_auth_core/application/use_cases/remove_role.py +103 -0
  28. baobab_auth_core/application/use_cases/revoke_all_sessions.py +94 -0
  29. baobab_auth_core/application/use_cases/revoke_session.py +97 -0
  30. baobab_auth_core/domain/__init__.py +1 -0
  31. baobab_auth_core/domain/entities/__init__.py +10 -0
  32. baobab_auth_core/domain/entities/audit_event.py +50 -0
  33. baobab_auth_core/domain/entities/permission.py +49 -0
  34. baobab_auth_core/domain/entities/role.py +69 -0
  35. baobab_auth_core/domain/entities/session.py +91 -0
  36. baobab_auth_core/domain/entities/user.py +144 -0
  37. baobab_auth_core/domain/entities/user_profile.py +103 -0
  38. baobab_auth_core/domain/enums/__init__.py +7 -0
  39. baobab_auth_core/domain/enums/audit_event_type.py +27 -0
  40. baobab_auth_core/domain/enums/session_status.py +14 -0
  41. baobab_auth_core/domain/enums/user_status.py +16 -0
  42. baobab_auth_core/domain/policies/__init__.py +15 -0
  43. baobab_auth_core/domain/policies/lockout_policy.py +25 -0
  44. baobab_auth_core/domain/policies/password_policy.py +62 -0
  45. baobab_auth_core/domain/policies/permission_policy.py +15 -0
  46. baobab_auth_core/domain/policies/role_policy.py +19 -0
  47. baobab_auth_core/domain/policies/session_policy.py +37 -0
  48. baobab_auth_core/domain/ports/__init__.py +25 -0
  49. baobab_auth_core/domain/ports/audit_repository.py +29 -0
  50. baobab_auth_core/domain/ports/clock.py +20 -0
  51. baobab_auth_core/domain/ports/id_generator.py +18 -0
  52. baobab_auth_core/domain/ports/password_hasher.py +31 -0
  53. baobab_auth_core/domain/ports/permission_repository.py +51 -0
  54. baobab_auth_core/domain/ports/role_repository.py +51 -0
  55. baobab_auth_core/domain/ports/session_repository.py +46 -0
  56. baobab_auth_core/domain/ports/token_provider.py +20 -0
  57. baobab_auth_core/domain/ports/unit_of_work.py +48 -0
  58. baobab_auth_core/domain/ports/user_repository.py +60 -0
  59. baobab_auth_core/domain/value_objects/__init__.py +23 -0
  60. baobab_auth_core/domain/value_objects/auth_subject.py +24 -0
  61. baobab_auth_core/domain/value_objects/email.py +27 -0
  62. baobab_auth_core/domain/value_objects/password.py +26 -0
  63. baobab_auth_core/domain/value_objects/password_hash.py +25 -0
  64. baobab_auth_core/domain/value_objects/permission_name.py +27 -0
  65. baobab_auth_core/domain/value_objects/role_name.py +27 -0
  66. baobab_auth_core/domain/value_objects/session_id.py +24 -0
  67. baobab_auth_core/domain/value_objects/token_id.py +24 -0
  68. baobab_auth_core/domain/value_objects/utc_datetime.py +27 -0
  69. baobab_auth_core/exceptions/__init__.py +74 -0
  70. baobab_auth_core/exceptions/authentication.py +23 -0
  71. baobab_auth_core/exceptions/authorization.py +19 -0
  72. baobab_auth_core/exceptions/base.py +10 -0
  73. baobab_auth_core/exceptions/role.py +19 -0
  74. baobab_auth_core/exceptions/session.py +15 -0
  75. baobab_auth_core/exceptions/user.py +27 -0
  76. baobab_auth_core/exceptions/validation.py +19 -0
  77. baobab_auth_core/testing/__init__.py +29 -0
  78. baobab_auth_core/testing/fake_clock.py +34 -0
  79. baobab_auth_core/testing/fake_id_generator.py +25 -0
  80. baobab_auth_core/testing/fake_password_hasher.py +33 -0
  81. baobab_auth_core/testing/fake_token_provider.py +27 -0
  82. baobab_auth_core/testing/in_memory_audit_repository.py +32 -0
  83. baobab_auth_core/testing/in_memory_permission_repository.py +55 -0
  84. baobab_auth_core/testing/in_memory_role_repository.py +55 -0
  85. baobab_auth_core/testing/in_memory_session_repository.py +54 -0
  86. baobab_auth_core/testing/in_memory_unit_of_work.py +64 -0
  87. baobab_auth_core/testing/in_memory_user_repository.py +67 -0
  88. baobab_auth_core-0.4.0.dist-info/METADATA +247 -0
  89. baobab_auth_core-0.4.0.dist-info/RECORD +91 -0
  90. baobab_auth_core-0.4.0.dist-info/WHEEL +4 -0
  91. baobab_auth_core-0.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,111 @@
1
+ """baobab-auth-core — librairie métier d'authentification de l'écosystème Baobab."""
2
+
3
+ # --- Couche domaine : entités, énumérations, ports, politiques ---
4
+ # --- Couche application : commandes, résultats, services, cas d'usage ---
5
+ from baobab_auth_core.application.commands import (
6
+ AssignRoleCommand,
7
+ AuthenticateUserCommand,
8
+ ChangePasswordCommand,
9
+ LogoutCommand,
10
+ RefreshSessionCommand,
11
+ RegisterUserCommand,
12
+ RemoveRoleCommand,
13
+ RevokeAllSessionsCommand,
14
+ RevokeSessionCommand,
15
+ )
16
+ from baobab_auth_core.application.results import (
17
+ AuthContext,
18
+ AuthenticateUserResult,
19
+ RefreshSessionResult,
20
+ RegisterUserResult,
21
+ )
22
+ from baobab_auth_core.application.services import AuthorizationService
23
+ from baobab_auth_core.application.use_cases import (
24
+ AssignRole,
25
+ AuthenticateUser,
26
+ ChangePassword,
27
+ Logout,
28
+ RefreshSession,
29
+ RegisterUser,
30
+ RemoveRole,
31
+ RevokeAllSessions,
32
+ RevokeSession,
33
+ )
34
+ from baobab_auth_core.domain.entities import (
35
+ AuditEvent,
36
+ Permission,
37
+ Role,
38
+ Session,
39
+ User,
40
+ UserProfile,
41
+ )
42
+ from baobab_auth_core.domain.enums import AuditEventType, SessionStatus, UserStatus
43
+ from baobab_auth_core.domain.policies import (
44
+ LockoutPolicy,
45
+ PasswordPolicy,
46
+ PermissionPolicy,
47
+ RolePolicy,
48
+ SessionPolicy,
49
+ )
50
+ from baobab_auth_core.domain.ports import (
51
+ AuditRepository,
52
+ Clock,
53
+ IdGenerator,
54
+ PasswordHasher,
55
+ PermissionRepository,
56
+ RoleRepository,
57
+ SessionRepository,
58
+ TokenProvider,
59
+ UnitOfWork,
60
+ UserRepository,
61
+ )
62
+
63
+ __all__ = [
64
+ "AssignRole",
65
+ "AssignRoleCommand",
66
+ "AuditEvent",
67
+ "AuditEventType",
68
+ "AuditRepository",
69
+ "AuthContext",
70
+ "AuthenticateUser",
71
+ "AuthenticateUserCommand",
72
+ "AuthenticateUserResult",
73
+ "AuthorizationService",
74
+ "ChangePassword",
75
+ "ChangePasswordCommand",
76
+ "Clock",
77
+ "IdGenerator",
78
+ "LockoutPolicy",
79
+ "Logout",
80
+ "LogoutCommand",
81
+ "PasswordHasher",
82
+ "PasswordPolicy",
83
+ "Permission",
84
+ "PermissionPolicy",
85
+ "PermissionRepository",
86
+ "RefreshSession",
87
+ "RefreshSessionCommand",
88
+ "RefreshSessionResult",
89
+ "RegisterUser",
90
+ "RegisterUserCommand",
91
+ "RegisterUserResult",
92
+ "RemoveRole",
93
+ "RemoveRoleCommand",
94
+ "RevokeAllSessions",
95
+ "RevokeAllSessionsCommand",
96
+ "RevokeSession",
97
+ "RevokeSessionCommand",
98
+ "Role",
99
+ "RolePolicy",
100
+ "RoleRepository",
101
+ "Session",
102
+ "SessionPolicy",
103
+ "SessionRepository",
104
+ "SessionStatus",
105
+ "TokenProvider",
106
+ "UnitOfWork",
107
+ "User",
108
+ "UserProfile",
109
+ "UserRepository",
110
+ "UserStatus",
111
+ ]
@@ -0,0 +1 @@
1
+ """Couche application : commandes, résultats et cas d'usage."""
@@ -0,0 +1,35 @@
1
+ """Commandes de la couche application."""
2
+
3
+ from baobab_auth_core.application.commands.assign_role_command import AssignRoleCommand
4
+ from baobab_auth_core.application.commands.authenticate_user_command import (
5
+ AuthenticateUserCommand,
6
+ )
7
+ from baobab_auth_core.application.commands.change_password_command import (
8
+ ChangePasswordCommand,
9
+ )
10
+ from baobab_auth_core.application.commands.logout_command import LogoutCommand
11
+ from baobab_auth_core.application.commands.refresh_session_command import (
12
+ RefreshSessionCommand,
13
+ )
14
+ from baobab_auth_core.application.commands.register_user_command import (
15
+ RegisterUserCommand,
16
+ )
17
+ from baobab_auth_core.application.commands.remove_role_command import RemoveRoleCommand
18
+ from baobab_auth_core.application.commands.revoke_all_sessions_command import (
19
+ RevokeAllSessionsCommand,
20
+ )
21
+ from baobab_auth_core.application.commands.revoke_session_command import (
22
+ RevokeSessionCommand,
23
+ )
24
+
25
+ __all__ = [
26
+ "AssignRoleCommand",
27
+ "AuthenticateUserCommand",
28
+ "ChangePasswordCommand",
29
+ "LogoutCommand",
30
+ "RefreshSessionCommand",
31
+ "RegisterUserCommand",
32
+ "RemoveRoleCommand",
33
+ "RevokeAllSessionsCommand",
34
+ "RevokeSessionCommand",
35
+ ]
@@ -0,0 +1,22 @@
1
+ """Commande : attribution d'un rôle à un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class AssignRoleCommand:
8
+ """Commande d'attribution d'un rôle à un utilisateur cible.
9
+
10
+ :param target_user_id: identifiant interne de l'utilisateur cible.
11
+ :param role_name: nom du rôle à attribuer.
12
+ :param actor_subject: sujet d'authentification de l'acteur (doit être ADMIN+).
13
+ :param ip_address: adresse IP de la requête (audit).
14
+ :param user_agent: user-agent de la requête (audit).
15
+ :spec: FEAT-003.1
16
+ """
17
+
18
+ target_user_id: str
19
+ role_name: str
20
+ actor_subject: str
21
+ ip_address: str
22
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande d'authentification d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class AuthenticateUserCommand:
8
+ """Données d'entrée pour le cas d'usage AuthenticateUser.
9
+
10
+ :param email: adresse e-mail brute de l'utilisateur.
11
+ :param password: mot de passe en clair.
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-002.2
15
+ """
16
+
17
+ email: str
18
+ password: str
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,22 @@
1
+ """Commande : changement de mot de passe par l'utilisateur lui-même."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class ChangePasswordCommand:
8
+ """Données d'entrée pour le cas d'usage ChangePassword.
9
+
10
+ :param actor_subject: sujet d'authentification de l'acteur.
11
+ :param old_password: mot de passe actuel (en clair).
12
+ :param new_password: nouveau mot de passe souhaité (en clair).
13
+ :param ip_address: adresse IP de l'appelant.
14
+ :param user_agent: user-agent de l'appelant.
15
+ :spec: FEAT-004.2
16
+ """
17
+
18
+ actor_subject: str
19
+ old_password: str
20
+ new_password: str
21
+ ip_address: str
22
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande de déconnexion d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class LogoutCommand:
8
+ """Données d'entrée pour le cas d'usage Logout.
9
+
10
+ :param session_id: identifiant de la session à révoquer.
11
+ :param actor_subject: sujet de l'acteur initiant la déconnexion (ou ``None``).
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-002.4
15
+ """
16
+
17
+ session_id: str
18
+ actor_subject: str | None
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande de rafraîchissement d'une session."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RefreshSessionCommand:
8
+ """Données d'entrée pour le cas d'usage RefreshSession.
9
+
10
+ :param session_id: identifiant de la session à rafraîchir.
11
+ :param refresh_token_id: valeur du refresh token présenté par le client.
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-002.3
15
+ """
16
+
17
+ session_id: str
18
+ refresh_token_id: str
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande d'inscription d'un nouvel utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RegisterUserCommand:
8
+ """Données d'entrée pour le cas d'usage RegisterUser.
9
+
10
+ :param email: adresse e-mail brute de l'utilisateur.
11
+ :param password: mot de passe en clair (validé et haché par le use case).
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-002.1
15
+ """
16
+
17
+ email: str
18
+ password: str
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,22 @@
1
+ """Commande : retrait d'un rôle d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RemoveRoleCommand:
8
+ """Commande de retrait d'un rôle d'un utilisateur cible.
9
+
10
+ :param target_user_id: identifiant interne de l'utilisateur cible.
11
+ :param role_name: nom du rôle à retirer.
12
+ :param actor_subject: sujet d'authentification de l'acteur (doit être ADMIN+).
13
+ :param ip_address: adresse IP de la requête (audit).
14
+ :param user_agent: user-agent de la requête (audit).
15
+ :spec: FEAT-003.2
16
+ """
17
+
18
+ target_user_id: str
19
+ role_name: str
20
+ actor_subject: str
21
+ ip_address: str
22
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande : révocation globale de toutes les sessions d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RevokeAllSessionsCommand:
8
+ """Données d'entrée pour le cas d'usage RevokeAllSessions.
9
+
10
+ :param target_user_id: identifiant interne de l'utilisateur cible.
11
+ :param actor_subject: sujet d'authentification de l'acteur.
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-004.3
15
+ """
16
+
17
+ target_user_id: str
18
+ actor_subject: str
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,20 @@
1
+ """Commande de révocation d'une session par un acteur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RevokeSessionCommand:
8
+ """Données d'entrée pour le cas d'usage RevokeSession.
9
+
10
+ :param session_id: identifiant de la session à révoquer.
11
+ :param actor_subject: sujet de l'acteur demandant la révocation.
12
+ :param ip_address: adresse IP de l'appelant.
13
+ :param user_agent: user-agent de l'appelant.
14
+ :spec: FEAT-002.5
15
+ """
16
+
17
+ session_id: str
18
+ actor_subject: str
19
+ ip_address: str
20
+ user_agent: str
@@ -0,0 +1,17 @@
1
+ """Résultats de la couche application."""
2
+
3
+ from baobab_auth_core.application.results.auth_context import AuthContext
4
+ from baobab_auth_core.application.results.authenticate_user_result import (
5
+ AuthenticateUserResult,
6
+ )
7
+ from baobab_auth_core.application.results.refresh_session_result import (
8
+ RefreshSessionResult,
9
+ )
10
+ from baobab_auth_core.application.results.register_user_result import RegisterUserResult
11
+
12
+ __all__ = [
13
+ "AuthContext",
14
+ "AuthenticateUserResult",
15
+ "RefreshSessionResult",
16
+ "RegisterUserResult",
17
+ ]
@@ -0,0 +1,40 @@
1
+ """Contexte d'autorisation d'un acteur authentifié."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from baobab_auth_core.domain.value_objects.auth_subject import AuthSubject
6
+ from baobab_auth_core.domain.value_objects.permission_name import PermissionName
7
+ from baobab_auth_core.domain.value_objects.role_name import RoleName
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class AuthContext:
12
+ """Contexte d'autorisation construit depuis le référentiel des rôles.
13
+
14
+ Regroupe les rôles et les permissions effectifs d'un acteur authentifié.
15
+
16
+ :param auth_subject: identifiant stable de l'acteur.
17
+ :param roles: ensemble figé des rôles de l'acteur.
18
+ :param permissions: ensemble figé des permissions cumulées.
19
+ :spec: FEAT-003.3
20
+ """
21
+
22
+ auth_subject: AuthSubject
23
+ roles: frozenset[RoleName]
24
+ permissions: frozenset[PermissionName]
25
+
26
+ def has_role(self, role: RoleName) -> bool:
27
+ """Retourne ``True`` si l'acteur possède le rôle donné.
28
+
29
+ :param role: rôle à vérifier.
30
+ :returns: ``True`` si le rôle est présent.
31
+ """
32
+ return role in self.roles
33
+
34
+ def has_permission(self, permission: PermissionName) -> bool:
35
+ """Retourne ``True`` si l'acteur possède la permission donnée.
36
+
37
+ :param permission: permission à vérifier.
38
+ :returns: ``True`` si la permission est présente.
39
+ """
40
+ return permission in self.permissions
@@ -0,0 +1,22 @@
1
+ """Résultat de l'authentification d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from baobab_auth_core.domain.value_objects.utc_datetime import UtcDatetime
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class AuthenticateUserResult:
10
+ """Résultat du cas d'usage AuthenticateUser.
11
+
12
+ :param session_id: identifiant de la session créée.
13
+ :param refresh_token_id: identifiant du refresh token émis.
14
+ :param user_id: identifiant de l'utilisateur authentifié.
15
+ :param expires_at: date d'expiration de la session (UTC).
16
+ :spec: FEAT-002.2
17
+ """
18
+
19
+ session_id: str
20
+ refresh_token_id: str
21
+ user_id: str
22
+ expires_at: UtcDatetime
@@ -0,0 +1,20 @@
1
+ """Résultat du rafraîchissement d'une session."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from baobab_auth_core.domain.value_objects.utc_datetime import UtcDatetime
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class RefreshSessionResult:
10
+ """Résultat du cas d'usage RefreshSession.
11
+
12
+ :param session_id: identifiant de la session rafraîchie.
13
+ :param refresh_token_id: nouvel identifiant du refresh token (rotation).
14
+ :param expires_at: date d'expiration de la session (UTC).
15
+ :spec: FEAT-002.3
16
+ """
17
+
18
+ session_id: str
19
+ refresh_token_id: str
20
+ expires_at: UtcDatetime
@@ -0,0 +1,16 @@
1
+ """Résultat de l'inscription d'un utilisateur."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class RegisterUserResult:
8
+ """Résultat du cas d'usage RegisterUser.
9
+
10
+ :param user_id: identifiant interne du nouvel utilisateur.
11
+ :param auth_subject: sujet d'authentification opaque du nouvel utilisateur.
12
+ :spec: FEAT-002.1
13
+ """
14
+
15
+ user_id: str
16
+ auth_subject: str
@@ -0,0 +1,7 @@
1
+ """Services applicatifs (lecture, autorisation)."""
2
+
3
+ from baobab_auth_core.application.services.authorization_service import (
4
+ AuthorizationService,
5
+ )
6
+
7
+ __all__ = ["AuthorizationService"]
@@ -0,0 +1,56 @@
1
+ """Service d'autorisation : construction du contexte d'autorisation."""
2
+
3
+ from baobab_auth_core.application.results.auth_context import AuthContext
4
+ from baobab_auth_core.domain.ports.role_repository import RoleRepository
5
+ from baobab_auth_core.domain.ports.user_repository import UserRepository
6
+ from baobab_auth_core.domain.value_objects.auth_subject import AuthSubject
7
+ from baobab_auth_core.domain.value_objects.permission_name import PermissionName
8
+ from baobab_auth_core.exceptions.user import UserNotFoundError
9
+
10
+
11
+ class AuthorizationService:
12
+ """Construit le contexte d'autorisation d'un acteur authentifié.
13
+
14
+ Agrège les rôles de l'utilisateur et les permissions associées à chaque rôle
15
+ pour produire un :class:`AuthContext` utilisable dans les guards.
16
+
17
+ :param users: référentiel des utilisateurs.
18
+ :param roles: référentiel des rôles (pour résoudre les permissions).
19
+ :spec: FEAT-003.3
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ users: UserRepository,
26
+ roles: RoleRepository,
27
+ ) -> None:
28
+ """Initialise le service avec ses dépendances."""
29
+ self._users = users
30
+ self._roles = roles
31
+
32
+ def build_context(self, auth_subject: AuthSubject) -> AuthContext:
33
+ """Construit le contexte d'autorisation pour un acteur.
34
+
35
+ Charge l'utilisateur, puis résout les permissions de chacun de ses rôles.
36
+
37
+ :param auth_subject: identifiant de l'acteur.
38
+ :returns: contexte d'autorisation peuplé.
39
+ :raises UserNotFoundError: si l'acteur est introuvable.
40
+ :spec: FEAT-003.3
41
+ """
42
+ user = self._users.get_by_auth_subject(auth_subject)
43
+ if user is None:
44
+ raise UserNotFoundError(f"utilisateur {auth_subject.value!r} introuvable")
45
+
46
+ permissions: set[PermissionName] = set()
47
+ for role_name in user.roles:
48
+ role = self._roles.get_by_name(role_name)
49
+ if role is not None:
50
+ permissions |= role.permissions
51
+
52
+ return AuthContext(
53
+ auth_subject=auth_subject,
54
+ roles=frozenset(user.roles),
55
+ permissions=frozenset(permissions),
56
+ )
@@ -0,0 +1,23 @@
1
+ """Cas d'usage de la couche application."""
2
+
3
+ from baobab_auth_core.application.use_cases.assign_role import AssignRole
4
+ from baobab_auth_core.application.use_cases.authenticate_user import AuthenticateUser
5
+ from baobab_auth_core.application.use_cases.change_password import ChangePassword
6
+ from baobab_auth_core.application.use_cases.logout import Logout
7
+ from baobab_auth_core.application.use_cases.refresh_session import RefreshSession
8
+ from baobab_auth_core.application.use_cases.register_user import RegisterUser
9
+ from baobab_auth_core.application.use_cases.remove_role import RemoveRole
10
+ from baobab_auth_core.application.use_cases.revoke_all_sessions import RevokeAllSessions
11
+ from baobab_auth_core.application.use_cases.revoke_session import RevokeSession
12
+
13
+ __all__ = [
14
+ "AssignRole",
15
+ "AuthenticateUser",
16
+ "ChangePassword",
17
+ "Logout",
18
+ "RefreshSession",
19
+ "RegisterUser",
20
+ "RemoveRole",
21
+ "RevokeAllSessions",
22
+ "RevokeSession",
23
+ ]
@@ -0,0 +1,96 @@
1
+ """Cas d'usage : attribution d'un rôle à un utilisateur."""
2
+
3
+ from baobab_auth_core.application.commands.assign_role_command import AssignRoleCommand
4
+ from baobab_auth_core.domain.entities.audit_event import AuditEvent
5
+ from baobab_auth_core.domain.enums.audit_event_type import AuditEventType
6
+ from baobab_auth_core.domain.ports.audit_repository import AuditRepository
7
+ from baobab_auth_core.domain.ports.clock import Clock
8
+ from baobab_auth_core.domain.ports.id_generator import IdGenerator
9
+ from baobab_auth_core.domain.ports.role_repository import RoleRepository
10
+ from baobab_auth_core.domain.ports.unit_of_work import UnitOfWork
11
+ from baobab_auth_core.domain.ports.user_repository import UserRepository
12
+ from baobab_auth_core.domain.value_objects.auth_subject import AuthSubject
13
+ from baobab_auth_core.domain.value_objects.role_name import RoleName
14
+ from baobab_auth_core.exceptions.authorization import ForbiddenError
15
+ from baobab_auth_core.exceptions.role import RoleNotFoundError
16
+ from baobab_auth_core.exceptions.user import UserNotFoundError
17
+
18
+ _ADMIN_ROLES = {RoleName(value="ADMIN"), RoleName(value="SUPER_ADMIN")}
19
+
20
+
21
+ class AssignRole:
22
+ """Attribue un rôle à un utilisateur au nom d'un acteur autorisé.
23
+
24
+ L'acteur doit posséder le rôle ``ADMIN`` ou ``SUPER_ADMIN``.
25
+ L'opération est idempotente : attribuer un rôle déjà présent n'a pas d'effet.
26
+
27
+ :param users: référentiel des utilisateurs.
28
+ :param roles: référentiel des rôles.
29
+ :param id_gen: générateur d'identifiants (audit).
30
+ :param clock: source de temps courante.
31
+ :param audit: référentiel d'événements d'audit.
32
+ :param uow: gestionnaire de transaction.
33
+ :spec: FEAT-003.1
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ users: UserRepository,
40
+ roles: RoleRepository,
41
+ id_gen: IdGenerator,
42
+ clock: Clock,
43
+ audit: AuditRepository,
44
+ uow: UnitOfWork,
45
+ ) -> None:
46
+ """Initialise le use case avec ses dépendances."""
47
+ self._users = users
48
+ self._roles = roles
49
+ self._id_gen = id_gen
50
+ self._clock = clock
51
+ self._audit = audit
52
+ self._uow = uow
53
+
54
+ def execute(self, command: AssignRoleCommand) -> None:
55
+ """Exécute l'attribution d'un rôle.
56
+
57
+ :param command: données d'entrée de l'attribution.
58
+ :raises ForbiddenError: si l'acteur n'est pas ADMIN ni SUPER_ADMIN.
59
+ :raises UserNotFoundError: si l'utilisateur cible est introuvable.
60
+ :raises RoleNotFoundError: si le rôle n'existe pas.
61
+ :spec: FEAT-003.1
62
+ """
63
+ now = self._clock.now()
64
+ actor_subject = AuthSubject(value=command.actor_subject)
65
+
66
+ actor = self._users.get_by_auth_subject(actor_subject)
67
+ if actor is None or not bool(actor.roles & _ADMIN_ROLES):
68
+ raise ForbiddenError("accès refusé : droits insuffisants")
69
+
70
+ target = self._users.get_by_id(command.target_user_id)
71
+ if target is None:
72
+ raise UserNotFoundError(f"utilisateur {command.target_user_id!r} introuvable")
73
+
74
+ role_name = RoleName(value=command.role_name)
75
+ if self._roles.get_by_name(role_name) is None:
76
+ raise RoleNotFoundError(f"rôle {command.role_name!r} introuvable")
77
+
78
+ target.assign_role(role_name, now)
79
+
80
+ with self._uow:
81
+ self._users.save(target)
82
+ self._audit.save(
83
+ AuditEvent(
84
+ id=self._id_gen.generate(),
85
+ actor_subject=actor_subject,
86
+ event_type=AuditEventType.ROLE_ASSIGNED,
87
+ target_type="user",
88
+ target_id=target.id,
89
+ ip_address=command.ip_address,
90
+ user_agent=command.user_agent,
91
+ metadata={"role": command.role_name},
92
+ created_at=now,
93
+ severity="INFO",
94
+ )
95
+ )
96
+ self._uow.commit()