baobab-auth-core 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_core/__init__.py +13 -0
- baobab_auth_core/application/__init__.py +6 -0
- baobab_auth_core/application/commands/__init__.py +6 -0
- baobab_auth_core/application/results/__init__.py +6 -0
- baobab_auth_core/application/use_cases/__init__.py +6 -0
- baobab_auth_core/domain/__init__.py +6 -0
- baobab_auth_core/domain/entities/__init__.py +20 -0
- baobab_auth_core/domain/entities/audit_event.py +46 -0
- baobab_auth_core/domain/entities/permission.py +32 -0
- baobab_auth_core/domain/entities/role.py +33 -0
- baobab_auth_core/domain/entities/session.py +42 -0
- baobab_auth_core/domain/entities/user.py +157 -0
- baobab_auth_core/domain/entities/user_profile.py +34 -0
- baobab_auth_core/domain/enums/__init__.py +18 -0
- baobab_auth_core/domain/enums/audit_event_type.py +37 -0
- baobab_auth_core/domain/enums/audit_severity.py +19 -0
- baobab_auth_core/domain/enums/session_status.py +20 -0
- baobab_auth_core/domain/enums/user_status.py +23 -0
- baobab_auth_core/domain/policies/__init__.py +18 -0
- baobab_auth_core/domain/policies/password_policy.py +80 -0
- baobab_auth_core/domain/policies/role_policy.py +30 -0
- baobab_auth_core/domain/policies/session_policy.py +33 -0
- baobab_auth_core/domain/services/__init__.py +6 -0
- baobab_auth_core/domain/value_objects/__init__.py +44 -0
- baobab_auth_core/domain/value_objects/audit_event_id.py +43 -0
- baobab_auth_core/domain/value_objects/auth_subject.py +44 -0
- baobab_auth_core/domain/value_objects/email.py +46 -0
- baobab_auth_core/domain/value_objects/password_hash.py +44 -0
- baobab_auth_core/domain/value_objects/permission_id.py +43 -0
- baobab_auth_core/domain/value_objects/permission_name.py +49 -0
- baobab_auth_core/domain/value_objects/plain_password.py +44 -0
- baobab_auth_core/domain/value_objects/role_id.py +43 -0
- baobab_auth_core/domain/value_objects/role_name.py +49 -0
- baobab_auth_core/domain/value_objects/session_id.py +43 -0
- baobab_auth_core/domain/value_objects/token_id.py +43 -0
- baobab_auth_core/domain/value_objects/user_id.py +43 -0
- baobab_auth_core/exceptions/__init__.py +98 -0
- baobab_auth_core/exceptions/auth.py +29 -0
- baobab_auth_core/exceptions/authorization.py +27 -0
- baobab_auth_core/exceptions/base.py +22 -0
- baobab_auth_core/exceptions/role.py +29 -0
- baobab_auth_core/exceptions/session.py +27 -0
- baobab_auth_core/exceptions/user.py +41 -0
- baobab_auth_core/exceptions/validation.py +43 -0
- baobab_auth_core/ports/__init__.py +32 -0
- baobab_auth_core/ports/audit_repository.py +33 -0
- baobab_auth_core/ports/clock.py +22 -0
- baobab_auth_core/ports/id_generator.py +21 -0
- baobab_auth_core/ports/password_hasher.py +35 -0
- baobab_auth_core/ports/permission_repository.py +45 -0
- baobab_auth_core/ports/role_repository.py +45 -0
- baobab_auth_core/ports/session_repository.py +45 -0
- baobab_auth_core/ports/token_provider.py +49 -0
- baobab_auth_core/ports/unit_of_work.py +58 -0
- baobab_auth_core/ports/user_repository.py +65 -0
- baobab_auth_core/py.typed +0 -0
- baobab_auth_core/testing/__init__.py +46 -0
- baobab_auth_core/testing/fake_clock.py +45 -0
- baobab_auth_core/testing/fake_id_generator.py +36 -0
- baobab_auth_core/testing/fake_password_hasher.py +34 -0
- baobab_auth_core/testing/fake_token_provider.py +66 -0
- baobab_auth_core/testing/in_memory_audit_repository.py +42 -0
- baobab_auth_core/testing/in_memory_permission_repository.py +53 -0
- baobab_auth_core/testing/in_memory_role_repository.py +53 -0
- baobab_auth_core/testing/in_memory_session_repository.py +55 -0
- baobab_auth_core/testing/in_memory_unit_of_work.py +68 -0
- baobab_auth_core/testing/in_memory_user_repository.py +76 -0
- baobab_auth_core-0.1.0.dist-info/METADATA +206 -0
- baobab_auth_core-0.1.0.dist-info/RECORD +71 -0
- baobab_auth_core-0.1.0.dist-info/WHEEL +4 -0
- baobab_auth_core-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""baobab-auth-core — socle métier d'authentification et d'autorisation.
|
|
2
|
+
|
|
3
|
+
Version : 0.1.0
|
|
4
|
+
|
|
5
|
+
Ce package expose les entités, value objects, policies, ports et fakes
|
|
6
|
+
de test du domaine d'authentification. Il ne contient aucune dépendance
|
|
7
|
+
sur des technologies concrètes (ORM, framework web, JWT, Argon2, etc.).
|
|
8
|
+
|
|
9
|
+
:spec: BL-010-001
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Entités du domaine baobab-auth-core.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from baobab_auth_core.domain.entities.audit_event import AuditEvent as AuditEvent
|
|
7
|
+
from baobab_auth_core.domain.entities.permission import Permission as Permission
|
|
8
|
+
from baobab_auth_core.domain.entities.role import Role as Role
|
|
9
|
+
from baobab_auth_core.domain.entities.session import Session as Session
|
|
10
|
+
from baobab_auth_core.domain.entities.user import User as User
|
|
11
|
+
from baobab_auth_core.domain.entities.user_profile import UserProfile as UserProfile
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AuditEvent",
|
|
15
|
+
"Permission",
|
|
16
|
+
"Role",
|
|
17
|
+
"Session",
|
|
18
|
+
"User",
|
|
19
|
+
"UserProfile",
|
|
20
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Entité AuditEvent — événement d'audit immuable.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from baobab_auth_core.domain.enums.audit_event_type import AuditEventType
|
|
12
|
+
from baobab_auth_core.domain.enums.audit_severity import AuditSeverity
|
|
13
|
+
from baobab_auth_core.domain.value_objects.audit_event_id import AuditEventId
|
|
14
|
+
from baobab_auth_core.domain.value_objects.auth_subject import AuthSubject
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AuditEvent:
|
|
19
|
+
"""Événement d'audit immuable tracé dans le journal de sécurité.
|
|
20
|
+
|
|
21
|
+
Cette entité est frozen (immuable) car les événements d'audit
|
|
22
|
+
ne doivent jamais être modifiés après enregistrement.
|
|
23
|
+
|
|
24
|
+
:param id: Identifiant unique de l'événement.
|
|
25
|
+
:param event_type: Type de l'événement.
|
|
26
|
+
:param severity: Niveau de sévérité.
|
|
27
|
+
:param created_at: Horodatage de l'événement (UTC).
|
|
28
|
+
:param actor_subject: Sujet initiateur de l'action
|
|
29
|
+
(ou None pour des actions système).
|
|
30
|
+
:param target_type: Type de la ressource ciblée (ex. ``user``, ``session``).
|
|
31
|
+
:param target_id: Identifiant de la ressource ciblée.
|
|
32
|
+
:param ip_address: Adresse IP de l'acteur.
|
|
33
|
+
:param user_agent: User-Agent HTTP de l'acteur.
|
|
34
|
+
:param metadata: Métadonnées additionnelles contextuelles.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
id: AuditEventId
|
|
38
|
+
event_type: AuditEventType
|
|
39
|
+
severity: AuditSeverity
|
|
40
|
+
created_at: datetime
|
|
41
|
+
actor_subject: AuthSubject | None = field(default=None)
|
|
42
|
+
target_type: str | None = field(default=None)
|
|
43
|
+
target_id: str | None = field(default=None)
|
|
44
|
+
ip_address: str | None = field(default=None)
|
|
45
|
+
user_agent: str | None = field(default=None)
|
|
46
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Entité Permission — permission atomique du système d'autorisation.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from baobab_auth_core.domain.value_objects.permission_id import PermissionId
|
|
10
|
+
from baobab_auth_core.domain.value_objects.permission_name import PermissionName
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Permission:
|
|
15
|
+
"""Permission atomique du système d'autorisation.
|
|
16
|
+
|
|
17
|
+
:param id: Identifiant unique de la permission.
|
|
18
|
+
:param name: Nom unique au format ``scope:resource:action``.
|
|
19
|
+
:param resource: Ressource ciblée.
|
|
20
|
+
:param action: Action autorisée sur la ressource.
|
|
21
|
+
:param is_system: Si ``True``, permission système non supprimable.
|
|
22
|
+
:param created_at: Date de création (UTC).
|
|
23
|
+
:param description: Description optionnelle.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
id: PermissionId
|
|
27
|
+
name: PermissionName
|
|
28
|
+
resource: str
|
|
29
|
+
action: str
|
|
30
|
+
is_system: bool
|
|
31
|
+
created_at: datetime
|
|
32
|
+
description: str | None = field(default=None)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Entité Role — rôle du système d'autorisation.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from baobab_auth_core.domain.value_objects.permission_name import PermissionName
|
|
10
|
+
from baobab_auth_core.domain.value_objects.role_id import RoleId
|
|
11
|
+
from baobab_auth_core.domain.value_objects.role_name import RoleName
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Role:
|
|
16
|
+
"""Rôle du système d'autorisation regroupant un ensemble de permissions.
|
|
17
|
+
|
|
18
|
+
:param id: Identifiant unique du rôle.
|
|
19
|
+
:param name: Nom unique du rôle.
|
|
20
|
+
:param is_system: Si ``True``, rôle système non supprimable.
|
|
21
|
+
:param created_at: Date de création (UTC).
|
|
22
|
+
:param updated_at: Date de dernière mise à jour (UTC).
|
|
23
|
+
:param description: Description optionnelle.
|
|
24
|
+
:param permission_names: Permissions associées (sans doublon).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
id: RoleId
|
|
28
|
+
name: RoleName
|
|
29
|
+
is_system: bool
|
|
30
|
+
created_at: datetime
|
|
31
|
+
updated_at: datetime
|
|
32
|
+
description: str | None = field(default=None)
|
|
33
|
+
permission_names: tuple[PermissionName, ...] = field(default_factory=tuple)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Entité Session — session utilisateur active.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from baobab_auth_core.domain.enums.session_status import SessionStatus
|
|
10
|
+
from baobab_auth_core.domain.value_objects.session_id import SessionId
|
|
11
|
+
from baobab_auth_core.domain.value_objects.token_id import TokenId
|
|
12
|
+
from baobab_auth_core.domain.value_objects.user_id import UserId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Session:
|
|
17
|
+
"""Session utilisateur associant un token de rafraîchissement à un utilisateur.
|
|
18
|
+
|
|
19
|
+
:param id: Identifiant unique de la session.
|
|
20
|
+
:param user_id: Identifiant de l'utilisateur propriétaire.
|
|
21
|
+
:param refresh_token_id: Identifiant du token de rafraîchissement.
|
|
22
|
+
:param status: Statut courant de la session.
|
|
23
|
+
:param created_at: Date de création (UTC).
|
|
24
|
+
:param expires_at: Date d'expiration (UTC).
|
|
25
|
+
:param revoked_at: Date de révocation (UTC ou None).
|
|
26
|
+
:param last_used_at: Date de dernière utilisation (UTC ou None).
|
|
27
|
+
:param user_agent: User-Agent HTTP du client (ou None).
|
|
28
|
+
:param ip_address: Adresse IP du client (ou None).
|
|
29
|
+
:param device_label: Libellé lisible du device (ou None).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
id: SessionId
|
|
33
|
+
user_id: UserId
|
|
34
|
+
refresh_token_id: TokenId
|
|
35
|
+
status: SessionStatus
|
|
36
|
+
created_at: datetime
|
|
37
|
+
expires_at: datetime
|
|
38
|
+
revoked_at: datetime | None = field(default=None)
|
|
39
|
+
last_used_at: datetime | None = field(default=None)
|
|
40
|
+
user_agent: str | None = field(default=None)
|
|
41
|
+
ip_address: str | None = field(default=None)
|
|
42
|
+
device_label: str | None = field(default=None)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Entité User — compte utilisateur du domaine.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from baobab_auth_core.domain.enums.user_status import UserStatus
|
|
10
|
+
from baobab_auth_core.domain.value_objects.auth_subject import AuthSubject
|
|
11
|
+
from baobab_auth_core.domain.value_objects.email import Email
|
|
12
|
+
from baobab_auth_core.domain.value_objects.password_hash import PasswordHash
|
|
13
|
+
from baobab_auth_core.domain.value_objects.role_name import RoleName
|
|
14
|
+
from baobab_auth_core.domain.value_objects.user_id import UserId
|
|
15
|
+
from baobab_auth_core.exceptions.validation import ValidationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class User:
|
|
20
|
+
"""Compte utilisateur — agrégat racine du domaine d'authentification.
|
|
21
|
+
|
|
22
|
+
Invariants :
|
|
23
|
+
- ``email`` obligatoire et normalisé ;
|
|
24
|
+
- ``auth_subject`` obligatoire et stable ;
|
|
25
|
+
- ``password_hash`` jamais vide ;
|
|
26
|
+
- ``failed_login_count >= 0`` ;
|
|
27
|
+
- dates UTC aware ;
|
|
28
|
+
- rôles sans doublon.
|
|
29
|
+
|
|
30
|
+
:param id: Identifiant unique de l'utilisateur.
|
|
31
|
+
:param auth_subject: Identifiant stable du sujet d'authentification.
|
|
32
|
+
:param email: Adresse email normalisée.
|
|
33
|
+
:param password_hash: Hash du mot de passe.
|
|
34
|
+
:param status: Statut actuel du compte.
|
|
35
|
+
:param role_names: Rôles assignés (sans doublon).
|
|
36
|
+
:param created_at: Date de création (UTC).
|
|
37
|
+
:param updated_at: Date de dernière mise à jour (UTC).
|
|
38
|
+
:param last_login_at: Date de dernière connexion réussie (UTC ou None).
|
|
39
|
+
:param failed_login_count: Nombre de tentatives de connexion échouées consécutives.
|
|
40
|
+
:param locked_until: Date de fin de verrouillage (UTC ou None).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
id: UserId
|
|
44
|
+
auth_subject: AuthSubject
|
|
45
|
+
email: Email
|
|
46
|
+
password_hash: PasswordHash
|
|
47
|
+
status: UserStatus
|
|
48
|
+
role_names: tuple[RoleName, ...]
|
|
49
|
+
created_at: datetime
|
|
50
|
+
updated_at: datetime
|
|
51
|
+
last_login_at: datetime | None = field(default=None)
|
|
52
|
+
failed_login_count: int = field(default=0)
|
|
53
|
+
locked_until: datetime | None = field(default=None)
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
"""Valide les invariants à la construction.
|
|
57
|
+
|
|
58
|
+
:raises ValidationError: Si ``failed_login_count`` est négatif ou si
|
|
59
|
+
``role_names`` contient des doublons.
|
|
60
|
+
"""
|
|
61
|
+
if self.failed_login_count < 0:
|
|
62
|
+
raise ValidationError(
|
|
63
|
+
"failed_login_count doit être supérieur ou égal à zéro."
|
|
64
|
+
)
|
|
65
|
+
unique = list(dict.fromkeys(self.role_names))
|
|
66
|
+
if len(unique) != len(self.role_names):
|
|
67
|
+
raise ValidationError("role_names ne peut pas contenir de doublons.")
|
|
68
|
+
|
|
69
|
+
def activate(self, now: datetime) -> None:
|
|
70
|
+
"""Active le compte utilisateur.
|
|
71
|
+
|
|
72
|
+
:param now: Horodatage courant (UTC).
|
|
73
|
+
"""
|
|
74
|
+
self.status = UserStatus.ACTIVE
|
|
75
|
+
self.updated_at = now
|
|
76
|
+
|
|
77
|
+
def disable(self, now: datetime) -> None:
|
|
78
|
+
"""Désactive le compte utilisateur.
|
|
79
|
+
|
|
80
|
+
:param now: Horodatage courant (UTC).
|
|
81
|
+
"""
|
|
82
|
+
self.status = UserStatus.DISABLED
|
|
83
|
+
self.updated_at = now
|
|
84
|
+
|
|
85
|
+
def lock(self, until: datetime, now: datetime) -> None:
|
|
86
|
+
"""Verrouille le compte jusqu'à une date donnée.
|
|
87
|
+
|
|
88
|
+
:param until: Date de fin de verrouillage (UTC).
|
|
89
|
+
:param now: Horodatage courant (UTC).
|
|
90
|
+
"""
|
|
91
|
+
self.status = UserStatus.LOCKED
|
|
92
|
+
self.locked_until = until
|
|
93
|
+
self.updated_at = now
|
|
94
|
+
|
|
95
|
+
def unlock(self, now: datetime) -> None:
|
|
96
|
+
"""Déverrouille le compte et remet le compteur d'échecs à zéro.
|
|
97
|
+
|
|
98
|
+
:param now: Horodatage courant (UTC).
|
|
99
|
+
"""
|
|
100
|
+
self.status = UserStatus.ACTIVE
|
|
101
|
+
self.locked_until = None
|
|
102
|
+
self.failed_login_count = 0
|
|
103
|
+
self.updated_at = now
|
|
104
|
+
|
|
105
|
+
def mark_login_success(self, now: datetime) -> None:
|
|
106
|
+
"""Enregistre une connexion réussie et réinitialise le compteur d'échecs.
|
|
107
|
+
|
|
108
|
+
:param now: Horodatage courant (UTC).
|
|
109
|
+
"""
|
|
110
|
+
self.last_login_at = now
|
|
111
|
+
self.failed_login_count = 0
|
|
112
|
+
self.updated_at = now
|
|
113
|
+
|
|
114
|
+
def mark_login_failure(self, now: datetime) -> None:
|
|
115
|
+
"""Incrémente le compteur de tentatives de connexion échouées.
|
|
116
|
+
|
|
117
|
+
:param now: Horodatage courant (UTC).
|
|
118
|
+
"""
|
|
119
|
+
self.failed_login_count += 1
|
|
120
|
+
self.updated_at = now
|
|
121
|
+
|
|
122
|
+
def change_password_hash(self, password_hash: PasswordHash, now: datetime) -> None:
|
|
123
|
+
"""Remplace le hash de mot de passe.
|
|
124
|
+
|
|
125
|
+
:param password_hash: Nouveau hash.
|
|
126
|
+
:param now: Horodatage courant (UTC).
|
|
127
|
+
"""
|
|
128
|
+
self.password_hash = password_hash
|
|
129
|
+
self.updated_at = now
|
|
130
|
+
|
|
131
|
+
def assign_role(self, role_name: RoleName, now: datetime) -> None:
|
|
132
|
+
"""Assigne un rôle si non déjà présent.
|
|
133
|
+
|
|
134
|
+
:param role_name: Nom du rôle à assigner.
|
|
135
|
+
:param now: Horodatage courant (UTC).
|
|
136
|
+
"""
|
|
137
|
+
if role_name not in self.role_names:
|
|
138
|
+
self.role_names = (*self.role_names, role_name)
|
|
139
|
+
self.updated_at = now
|
|
140
|
+
|
|
141
|
+
def remove_role(self, role_name: RoleName, now: datetime) -> None:
|
|
142
|
+
"""Retire un rôle de l'utilisateur.
|
|
143
|
+
|
|
144
|
+
:param role_name: Nom du rôle à retirer.
|
|
145
|
+
:param now: Horodatage courant (UTC).
|
|
146
|
+
"""
|
|
147
|
+
if role_name in self.role_names:
|
|
148
|
+
self.role_names = tuple(r for r in self.role_names if r != role_name)
|
|
149
|
+
self.updated_at = now
|
|
150
|
+
|
|
151
|
+
def has_role(self, role_name: RoleName) -> bool:
|
|
152
|
+
"""Vérifie si l'utilisateur possède un rôle donné.
|
|
153
|
+
|
|
154
|
+
:param role_name: Nom du rôle à vérifier.
|
|
155
|
+
:returns: ``True`` si le rôle est présent.
|
|
156
|
+
"""
|
|
157
|
+
return role_name in self.role_names
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Entité UserProfile — profil public d'un utilisateur.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-003
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from baobab_auth_core.domain.value_objects.user_id import UserId
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class UserProfile:
|
|
14
|
+
"""Profil public d'un utilisateur, sans aucun secret.
|
|
15
|
+
|
|
16
|
+
Cette entité ne contient aucune information d'authentification
|
|
17
|
+
(pas de mot de passe, hash, token ou secret).
|
|
18
|
+
|
|
19
|
+
:param user_id: Identifiant de l'utilisateur associé.
|
|
20
|
+
:param display_name: Nom d'affichage public (ou None).
|
|
21
|
+
:param locale: Code de locale BCP-47 (ex. ``fr-FR``) ou None.
|
|
22
|
+
:param timezone: Fuseau horaire IANA (ex. ``Europe/Paris``) ou None.
|
|
23
|
+
:param avatar_url: URL publique de l'avatar (ou None).
|
|
24
|
+
:param created_at: Date de création (UTC).
|
|
25
|
+
:param updated_at: Date de dernière mise à jour (UTC).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
user_id: UserId
|
|
29
|
+
created_at: datetime
|
|
30
|
+
updated_at: datetime
|
|
31
|
+
display_name: str | None = None
|
|
32
|
+
locale: str | None = None
|
|
33
|
+
timezone: str | None = None
|
|
34
|
+
avatar_url: str | None = None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Énumérations du domaine baobab-auth-core.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from baobab_auth_core.domain.enums.audit_event_type import (
|
|
7
|
+
AuditEventType as AuditEventType,
|
|
8
|
+
)
|
|
9
|
+
from baobab_auth_core.domain.enums.audit_severity import AuditSeverity as AuditSeverity
|
|
10
|
+
from baobab_auth_core.domain.enums.session_status import SessionStatus as SessionStatus
|
|
11
|
+
from baobab_auth_core.domain.enums.user_status import UserStatus as UserStatus
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AuditEventType",
|
|
15
|
+
"AuditSeverity",
|
|
16
|
+
"SessionStatus",
|
|
17
|
+
"UserStatus",
|
|
18
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Types d'événements d'audit.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuditEventType(StrEnum):
|
|
10
|
+
"""Catalogue des types d'événements traçables.
|
|
11
|
+
|
|
12
|
+
:cvar USER_REGISTERED: Nouvel utilisateur enregistré.
|
|
13
|
+
:cvar LOGIN_SUCCESS: Connexion réussie.
|
|
14
|
+
:cvar LOGIN_FAILURE: Échec de connexion (identifiants invalides).
|
|
15
|
+
:cvar LOGOUT: Déconnexion explicite.
|
|
16
|
+
:cvar SESSION_REFRESHED: Token de session renouvelé.
|
|
17
|
+
:cvar SESSION_REVOKED: Session révoquée.
|
|
18
|
+
:cvar ROLE_ASSIGNED: Rôle attribué à un utilisateur.
|
|
19
|
+
:cvar ROLE_REMOVED: Rôle retiré d'un utilisateur.
|
|
20
|
+
:cvar PASSWORD_CHANGED: Mot de passe modifié.
|
|
21
|
+
:cvar ACCOUNT_LOCKED: Compte verrouillé suite à trop d'échecs.
|
|
22
|
+
:cvar ACCOUNT_DISABLED: Compte désactivé par un administrateur.
|
|
23
|
+
:cvar ACCOUNT_DELETED: Compte supprimé.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
USER_REGISTERED = "USER_REGISTERED"
|
|
27
|
+
LOGIN_SUCCESS = "LOGIN_SUCCESS"
|
|
28
|
+
LOGIN_FAILURE = "LOGIN_FAILURE"
|
|
29
|
+
LOGOUT = "LOGOUT"
|
|
30
|
+
SESSION_REFRESHED = "SESSION_REFRESHED"
|
|
31
|
+
SESSION_REVOKED = "SESSION_REVOKED"
|
|
32
|
+
ROLE_ASSIGNED = "ROLE_ASSIGNED"
|
|
33
|
+
ROLE_REMOVED = "ROLE_REMOVED"
|
|
34
|
+
PASSWORD_CHANGED = "PASSWORD_CHANGED" # nosec B105
|
|
35
|
+
ACCOUNT_LOCKED = "ACCOUNT_LOCKED"
|
|
36
|
+
ACCOUNT_DISABLED = "ACCOUNT_DISABLED"
|
|
37
|
+
ACCOUNT_DELETED = "ACCOUNT_DELETED"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Niveau de sévérité d'un événement d'audit.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuditSeverity(StrEnum):
|
|
10
|
+
"""Sévérité d'un événement d'audit.
|
|
11
|
+
|
|
12
|
+
:cvar INFO: Événement informatif, opération normale.
|
|
13
|
+
:cvar WARNING: Situation anormale mais non critique.
|
|
14
|
+
:cvar CRITICAL: Incident de sécurité ou erreur grave.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
INFO = "INFO"
|
|
18
|
+
WARNING = "WARNING"
|
|
19
|
+
CRITICAL = "CRITICAL"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Statut d'une session utilisateur.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SessionStatus(StrEnum):
|
|
10
|
+
"""Cycle de vie d'une session.
|
|
11
|
+
|
|
12
|
+
:cvar ACTIVE: Session active et valide.
|
|
13
|
+
:cvar REVOKED: Session révoquée explicitement
|
|
14
|
+
(déconnexion, changement de mot de passe).
|
|
15
|
+
:cvar EXPIRED: Session expirée (TTL dépassé).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
ACTIVE = "ACTIVE"
|
|
19
|
+
REVOKED = "REVOKED"
|
|
20
|
+
EXPIRED = "EXPIRED"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Statut d'un compte utilisateur.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserStatus(StrEnum):
|
|
10
|
+
"""Cycle de vie d'un compte utilisateur.
|
|
11
|
+
|
|
12
|
+
:cvar PENDING: Compte créé, en attente de validation.
|
|
13
|
+
:cvar ACTIVE: Compte actif, l'utilisateur peut se connecter.
|
|
14
|
+
:cvar LOCKED: Compte temporairement verrouillé (trop d'échecs de connexion).
|
|
15
|
+
:cvar DISABLED: Compte désactivé par un administrateur.
|
|
16
|
+
:cvar DELETED: Compte supprimé (soft-delete).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
PENDING = "PENDING"
|
|
20
|
+
ACTIVE = "ACTIVE"
|
|
21
|
+
LOCKED = "LOCKED"
|
|
22
|
+
DISABLED = "DISABLED"
|
|
23
|
+
DELETED = "DELETED"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Politiques du domaine baobab-auth-core.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from baobab_auth_core.domain.policies.password_policy import (
|
|
7
|
+
PasswordPolicy as PasswordPolicy,
|
|
8
|
+
)
|
|
9
|
+
from baobab_auth_core.domain.policies.role_policy import RolePolicy as RolePolicy
|
|
10
|
+
from baobab_auth_core.domain.policies.session_policy import (
|
|
11
|
+
SessionPolicy as SessionPolicy,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PasswordPolicy",
|
|
16
|
+
"RolePolicy",
|
|
17
|
+
"SessionPolicy",
|
|
18
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Politique de mot de passe.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from baobab_auth_core.domain.value_objects.email import Email
|
|
9
|
+
from baobab_auth_core.domain.value_objects.plain_password import PlainPassword
|
|
10
|
+
from baobab_auth_core.exceptions.validation import WeakPasswordError
|
|
11
|
+
|
|
12
|
+
_DEFAULT_MIN_LENGTH = 12
|
|
13
|
+
_DEFAULT_MAX_LENGTH = 256
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PasswordPolicy:
|
|
18
|
+
"""Politique de validation des mots de passe.
|
|
19
|
+
|
|
20
|
+
Les valeurs par défaut correspondent au niveau de sécurité minimal
|
|
21
|
+
recommandé par OWASP.
|
|
22
|
+
|
|
23
|
+
:param min_length: Longueur minimale (défaut : 12).
|
|
24
|
+
:param max_length: Longueur maximale (défaut : 256).
|
|
25
|
+
:param require_letter: Exige au moins une lettre (défaut : True).
|
|
26
|
+
:param require_digit_or_symbol: Exige au moins un chiffre ou symbole
|
|
27
|
+
(défaut : True).
|
|
28
|
+
:param forbid_email_as_password: Interdit l'email comme mot de passe
|
|
29
|
+
(défaut : True).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
min_length: int = field(default=_DEFAULT_MIN_LENGTH)
|
|
33
|
+
max_length: int = field(default=_DEFAULT_MAX_LENGTH)
|
|
34
|
+
require_letter: bool = field(default=True)
|
|
35
|
+
require_digit_or_symbol: bool = field(default=True)
|
|
36
|
+
forbid_email_as_password: bool = field(default=True)
|
|
37
|
+
|
|
38
|
+
def validate(
|
|
39
|
+
self,
|
|
40
|
+
password: PlainPassword,
|
|
41
|
+
email: Email | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Valide un mot de passe selon la politique.
|
|
44
|
+
|
|
45
|
+
:param password: Mot de passe en clair à valider.
|
|
46
|
+
:param email: Adresse email de l'utilisateur (pour la règle d'exclusion).
|
|
47
|
+
:raises WeakPasswordError: Si le mot de passe ne respecte pas la politique.
|
|
48
|
+
"""
|
|
49
|
+
value = password.value
|
|
50
|
+
|
|
51
|
+
if len(value) < self.min_length:
|
|
52
|
+
raise WeakPasswordError(
|
|
53
|
+
f"Le mot de passe doit contenir au moins {self.min_length} caractères."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if len(value) > self.max_length:
|
|
57
|
+
raise WeakPasswordError(
|
|
58
|
+
f"Le mot de passe ne peut pas dépasser {self.max_length} caractères."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if self.require_letter and not any(c.isalpha() for c in value):
|
|
62
|
+
raise WeakPasswordError(
|
|
63
|
+
"Le mot de passe doit contenir au moins une lettre."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if self.require_digit_or_symbol and not any(
|
|
67
|
+
c.isdigit() or not c.isalnum() for c in value
|
|
68
|
+
):
|
|
69
|
+
raise WeakPasswordError(
|
|
70
|
+
"Le mot de passe doit contenir au moins un chiffre ou un symbole."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
self.forbid_email_as_password
|
|
75
|
+
and email is not None
|
|
76
|
+
and value.strip().lower() == email.value
|
|
77
|
+
):
|
|
78
|
+
raise WeakPasswordError(
|
|
79
|
+
"Le mot de passe ne peut pas être identique à l'adresse email."
|
|
80
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Politique de gestion des rôles.
|
|
2
|
+
|
|
3
|
+
:spec: BL-010-004
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from baobab_auth_core.domain.value_objects.role_name import RoleName
|
|
9
|
+
|
|
10
|
+
_DEFAULT_ROLE = "USER"
|
|
11
|
+
_SUPER_ADMIN_ROLE = "SUPER_ADMIN"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RolePolicy:
|
|
16
|
+
"""Politique de gestion des rôles et de la hiérarchie.
|
|
17
|
+
|
|
18
|
+
:param default_role_name: Rôle attribué automatiquement à tout nouvel utilisateur
|
|
19
|
+
(défaut : ``USER``).
|
|
20
|
+
:param super_admin_role_name: Nom du rôle super-administrateur
|
|
21
|
+
(défaut : ``SUPER_ADMIN``).
|
|
22
|
+
:param enforce_last_super_admin: Interdit la suppression du dernier super-admin
|
|
23
|
+
(défaut : True).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
default_role_name: RoleName = field(default_factory=lambda: RoleName(_DEFAULT_ROLE))
|
|
27
|
+
super_admin_role_name: RoleName = field(
|
|
28
|
+
default_factory=lambda: RoleName(_SUPER_ADMIN_ROLE)
|
|
29
|
+
)
|
|
30
|
+
enforce_last_super_admin: bool = field(default=True)
|