oxutils 0.1.8__py3-none-any.whl → 0.1.11__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.
- oxutils/__init__.py +2 -1
- oxutils/constants.py +4 -0
- oxutils/jwt/auth.py +150 -1
- oxutils/jwt/models.py +13 -0
- oxutils/logger/receivers.py +10 -6
- oxutils/models/base.py +102 -0
- oxutils/models/fields.py +79 -0
- oxutils/oxiliere/apps.py +6 -1
- oxutils/oxiliere/authorization.py +45 -0
- oxutils/oxiliere/caches.py +7 -7
- oxutils/oxiliere/checks.py +31 -0
- oxutils/oxiliere/constants.py +3 -0
- oxutils/oxiliere/context.py +16 -0
- oxutils/oxiliere/exceptions.py +16 -0
- oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
- oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
- oxutils/oxiliere/middleware.py +29 -13
- oxutils/oxiliere/models.py +130 -19
- oxutils/oxiliere/permissions.py +6 -5
- oxutils/oxiliere/schemas.py +13 -4
- oxutils/oxiliere/signals.py +5 -0
- oxutils/oxiliere/utils.py +14 -0
- oxutils/pagination/__init__.py +0 -0
- oxutils/pagination/cursor.py +367 -0
- oxutils/permissions/__init__.py +0 -0
- oxutils/permissions/actions.py +57 -0
- oxutils/permissions/admin.py +3 -0
- oxutils/permissions/apps.py +10 -0
- oxutils/permissions/caches.py +19 -0
- oxutils/permissions/checks.py +188 -0
- oxutils/permissions/constants.py +0 -0
- oxutils/permissions/controllers.py +344 -0
- oxutils/permissions/exceptions.py +60 -0
- oxutils/permissions/management/__init__.py +0 -0
- oxutils/permissions/management/commands/__init__.py +0 -0
- oxutils/permissions/management/commands/load_permission_preset.py +112 -0
- oxutils/permissions/migrations/0001_initial.py +112 -0
- oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
- oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
- oxutils/permissions/migrations/__init__.py +0 -0
- oxutils/permissions/models.py +171 -0
- oxutils/permissions/perms.py +95 -0
- oxutils/permissions/queryset.py +92 -0
- oxutils/permissions/schemas.py +276 -0
- oxutils/permissions/services.py +663 -0
- oxutils/permissions/tests.py +3 -0
- oxutils/permissions/utils.py +628 -0
- oxutils/settings.py +1 -0
- oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
- oxutils/users/models.py +2 -0
- {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/METADATA +1 -1
- {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/RECORD +53 -19
- {oxutils-0.1.8.dist-info → oxutils-0.1.11.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
from django.contrib.auth.models import AbstractBaseUser
|
|
3
|
+
from django.db import transaction
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from oxutils.mixins.services import BaseService
|
|
9
|
+
from oxutils.exceptions import NotFoundException
|
|
10
|
+
from .models import Grant, RoleGrant, Group, Role
|
|
11
|
+
from .utils import (
|
|
12
|
+
assign_role, revoke_role,
|
|
13
|
+
assign_group, revoke_group,
|
|
14
|
+
override_grant, check, group_sync
|
|
15
|
+
)
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
RoleNotFoundException,
|
|
18
|
+
GroupNotFoundException,
|
|
19
|
+
GrantNotFoundException,
|
|
20
|
+
RoleGrantNotFoundException,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
User = get_user_model()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = structlog.get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PermissionService(BaseService):
|
|
32
|
+
"""
|
|
33
|
+
Service pour la gestion des permissions.
|
|
34
|
+
Encapsule la logique métier liée aux rôles, groupes et grants.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def inner_exception_handler(self, exc: Exception, logger):
|
|
38
|
+
"""
|
|
39
|
+
Gère les exceptions spécifiques au service de permissions.
|
|
40
|
+
Convertit les exceptions métier en exceptions HTTP appropriées.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
exc: L'exception à gérer
|
|
44
|
+
logger: Logger pour la journalisation
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
APIException: Si l'exception est gérée
|
|
48
|
+
Exception: Re-lève l'exception originale si non gérée
|
|
49
|
+
"""
|
|
50
|
+
from oxutils.exceptions import APIException
|
|
51
|
+
|
|
52
|
+
# Si c'est déjà une APIException (incluant nos exceptions personnalisées),
|
|
53
|
+
# on la re-lève directement
|
|
54
|
+
if isinstance(exc, APIException):
|
|
55
|
+
raise exc
|
|
56
|
+
|
|
57
|
+
# Convertir les exceptions Django DoesNotExist en exceptions HTTP appropriées
|
|
58
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
59
|
+
|
|
60
|
+
if isinstance(exc, ObjectDoesNotExist):
|
|
61
|
+
# Déterminer le type d'objet pour un message plus précis
|
|
62
|
+
exc_name = type(exc).__name__
|
|
63
|
+
|
|
64
|
+
if 'Role' in exc_name:
|
|
65
|
+
raise RoleNotFoundException(detail=str(exc))
|
|
66
|
+
elif 'Group' in exc_name:
|
|
67
|
+
raise GroupNotFoundException(detail=str(exc))
|
|
68
|
+
elif 'Grant' in exc_name:
|
|
69
|
+
raise GrantNotFoundException(detail=str(exc))
|
|
70
|
+
elif 'RoleGrant' in exc_name:
|
|
71
|
+
raise RoleGrantNotFoundException(detail=str(exc))
|
|
72
|
+
else:
|
|
73
|
+
# Exception générique pour les autres cas
|
|
74
|
+
raise NotFoundException(detail=str(exc))
|
|
75
|
+
|
|
76
|
+
# Pour toutes les autres exceptions, laisser le handler parent gérer
|
|
77
|
+
raise exc
|
|
78
|
+
|
|
79
|
+
def get_roles(self):
|
|
80
|
+
return Role.objects.all()
|
|
81
|
+
|
|
82
|
+
def get_role(self, role_slug: str):
|
|
83
|
+
try:
|
|
84
|
+
return Role.objects.get(slug=role_slug)
|
|
85
|
+
except Role.DoesNotExist:
|
|
86
|
+
raise RoleNotFoundException(detail=f"Le rôle '{role_slug}' n'existe pas")
|
|
87
|
+
|
|
88
|
+
def get_user_roles(self, user: AbstractBaseUser):
|
|
89
|
+
return Role.objects.filter(grants__user__pk=user.pk)
|
|
90
|
+
|
|
91
|
+
def assign_role_to_user(
|
|
92
|
+
self,
|
|
93
|
+
user_id: int,
|
|
94
|
+
role_slug: str,
|
|
95
|
+
*,
|
|
96
|
+
by_user: Optional[AbstractBaseUser] = None
|
|
97
|
+
) -> Role:
|
|
98
|
+
"""
|
|
99
|
+
Assigne un rôle à un utilisateur.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
user: L'utilisateur à qui assigner le rôle
|
|
103
|
+
role_slug: Le slug du rôle à assigner
|
|
104
|
+
by_user: L'utilisateur qui effectue l'assignation (pour traçabilité)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionnaire avec les informations de l'assignation
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
NotFoundException: Si le rôle n'existe pas
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
user = User.objects.get(pk=user_id)
|
|
114
|
+
role = Role.objects.get(slug=role_slug)
|
|
115
|
+
|
|
116
|
+
assign_role(user, role_slug, by=by_user)
|
|
117
|
+
|
|
118
|
+
grants_count = Grant.objects.filter(user=user, role=role).count()
|
|
119
|
+
logger.info("role_assigned_to_user", user_id=user.pk, role=role_slug, grants_created=grants_count)
|
|
120
|
+
|
|
121
|
+
return role
|
|
122
|
+
|
|
123
|
+
except Role.DoesNotExist:
|
|
124
|
+
raise RoleNotFoundException(detail=f"Le rôle '{role_slug}' n'existe pas")
|
|
125
|
+
except User.DoesNotExist:
|
|
126
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {user_id} n'existe pas")
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
self.exception_handler(exc, self.logger)
|
|
129
|
+
|
|
130
|
+
def revoke_role_from_user(
|
|
131
|
+
self,
|
|
132
|
+
user_id: int,
|
|
133
|
+
role_slug: str
|
|
134
|
+
) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Révoque un rôle d'un utilisateur.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
user: L'utilisateur dont on révoque le rôle
|
|
140
|
+
role_slug: Le slug du rôle à révoquer
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionnaire avec les informations de la révocation
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
user = User.objects.get(pk=user_id)
|
|
147
|
+
deleted_count, _ = revoke_role(user, role_slug)
|
|
148
|
+
|
|
149
|
+
logger.info("role_revoked_from_user", user_id=user.pk, role=role_slug, grants_deleted=deleted_count)
|
|
150
|
+
|
|
151
|
+
except User.DoesNotExist:
|
|
152
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {user_id} n'existe pas")
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
self.exception_handler(exc, self.logger)
|
|
155
|
+
|
|
156
|
+
def assign_group_to_user(
|
|
157
|
+
self,
|
|
158
|
+
user_id: int,
|
|
159
|
+
group_slug: str,
|
|
160
|
+
by_user: Optional[AbstractBaseUser] = None
|
|
161
|
+
) -> list[Role]:
|
|
162
|
+
"""
|
|
163
|
+
Assigne tous les rôles d'un groupe à un utilisateur.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
user_id: L'ID de l'utilisateur à qui assigner le groupe
|
|
167
|
+
group_slug: Le slug du groupe à assigner
|
|
168
|
+
by_user: L'utilisateur qui effectue l'assignation (pour traçabilité)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Liste des rôles du groupe
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
NotFoundException: Si le groupe ou l'utilisateur n'existe pas
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
user = User.objects.get(pk=user_id)
|
|
178
|
+
group = Group.objects.prefetch_related('roles').get(slug=group_slug)
|
|
179
|
+
|
|
180
|
+
assign_group(user, group_slug, by=by_user)
|
|
181
|
+
|
|
182
|
+
logger.info("group_assigned_to_user", user_id=user.pk, group=group_slug, roles_assigned=group.roles.count())
|
|
183
|
+
|
|
184
|
+
return list(group.roles.all())
|
|
185
|
+
|
|
186
|
+
except Group.DoesNotExist:
|
|
187
|
+
raise GroupNotFoundException(detail=f"Le groupe '{group_slug}' n'existe pas")
|
|
188
|
+
except User.DoesNotExist:
|
|
189
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {user_id} n'existe pas")
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
self.exception_handler(exc, self.logger)
|
|
192
|
+
|
|
193
|
+
def revoke_group_from_user(
|
|
194
|
+
self,
|
|
195
|
+
user_id: int,
|
|
196
|
+
group_slug: str
|
|
197
|
+
) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Révoque tous les rôles d'un groupe d'un utilisateur.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
user_id: L'ID de l'utilisateur dont on révoque le groupe
|
|
203
|
+
group_slug: Le slug du groupe à révoquer
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
NotFoundException: Si le groupe ou l'utilisateur n'existe pas
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
user = User.objects.get(pk=user_id)
|
|
210
|
+
deleted_count, _ = revoke_group(user, group_slug)
|
|
211
|
+
|
|
212
|
+
logger.info("group_revoked_from_user", user_id=user.pk, group=group_slug, grants_deleted=deleted_count)
|
|
213
|
+
|
|
214
|
+
except User.DoesNotExist:
|
|
215
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {user_id} n'existe pas")
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
self.exception_handler(exc, self.logger)
|
|
218
|
+
|
|
219
|
+
def sync_group(self, group_slug: str) -> dict[str, int]:
|
|
220
|
+
"""
|
|
221
|
+
Synchronise les grants de tous les utilisateurs d'un groupe.
|
|
222
|
+
À appeler après modification des RoleGrants ou des rôles du groupe.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
group_slug: Le slug du groupe à synchroniser
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionnaire avec les statistiques de synchronisation
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
GroupNotFoundException: Si le groupe n'existe pas
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
stats = group_sync(group_slug)
|
|
235
|
+
|
|
236
|
+
logger.info("group_synced", group=group_slug, **stats)
|
|
237
|
+
|
|
238
|
+
return stats
|
|
239
|
+
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
self.exception_handler(exc, self.logger)
|
|
242
|
+
|
|
243
|
+
def override_user_grant(
|
|
244
|
+
self,
|
|
245
|
+
user: AbstractBaseUser,
|
|
246
|
+
scope: str,
|
|
247
|
+
remove_actions: list[str]
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""
|
|
250
|
+
Modifie un grant en retirant certaines actions.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
user: L'utilisateur dont on modifie le grant
|
|
254
|
+
scope: Le scope du grant à modifier
|
|
255
|
+
remove_actions: Liste des actions à retirer
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dictionnaire avec les informations de la modification
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
# Vérifier si le grant existe avant modification
|
|
262
|
+
grant_exists = Grant.objects.filter(user=user, scope=scope).exists()
|
|
263
|
+
|
|
264
|
+
if not grant_exists:
|
|
265
|
+
raise NotFoundException(
|
|
266
|
+
detail=f"Aucun grant trouvé pour l'utilisateur sur le scope '{scope}'"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
override_grant(user, scope, remove_actions)
|
|
270
|
+
|
|
271
|
+
# Vérifier si le grant existe toujours (peut avoir été supprimé)
|
|
272
|
+
grant_still_exists = Grant.objects.filter(user=user, scope=scope).exists()
|
|
273
|
+
|
|
274
|
+
logger.info("grant_modified", user_id=user.pk, scope=scope, removed_actions=remove_actions, grant_deleted=not grant_still_exists, grant_exists=grant_still_exists)
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"user_id": user.pk,
|
|
278
|
+
"scope": scope,
|
|
279
|
+
"removed_actions": remove_actions,
|
|
280
|
+
"grant_deleted": not grant_still_exists,
|
|
281
|
+
"message": "Grant modifié avec succès" if grant_still_exists else "Grant supprimé (plus d'actions)"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
self.exception_handler(exc, self.logger)
|
|
286
|
+
|
|
287
|
+
def check_permission(
|
|
288
|
+
self,
|
|
289
|
+
user_id: int,
|
|
290
|
+
scope: str,
|
|
291
|
+
required_actions: list[str],
|
|
292
|
+
context: dict[str, Any] = None
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Vérifie si un utilisateur possède les permissions requises.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
user_id: L'ID de l'utilisateur dont on vérifie les permissions
|
|
299
|
+
scope: Le scope à vérifier
|
|
300
|
+
required_actions: Liste des actions requises
|
|
301
|
+
context: Contexte additionnel pour filtrer les grants
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dictionnaire avec le résultat de la vérification
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
user = User.objects.get(pk=user_id)
|
|
308
|
+
allowed = check(user, scope, required_actions, **(context or {}))
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"allowed": allowed,
|
|
312
|
+
"user_id": user_id,
|
|
313
|
+
"scope": scope,
|
|
314
|
+
"required_actions": required_actions
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
except User.DoesNotExist:
|
|
318
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {user_id} n'existe pas")
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
self.exception_handler(exc, self.logger)
|
|
321
|
+
|
|
322
|
+
def get_user_grants(
|
|
323
|
+
self,
|
|
324
|
+
user: AbstractBaseUser,
|
|
325
|
+
scope: Optional[str] = None
|
|
326
|
+
) -> list[Grant]:
|
|
327
|
+
"""
|
|
328
|
+
Récupère tous les grants d'un utilisateur.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
user: L'utilisateur dont on récupère les grants
|
|
332
|
+
scope: Optionnel, filtre par scope
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Liste des grants de l'utilisateur
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
queryset = Grant.objects.filter(user=user).select_related('role')
|
|
339
|
+
|
|
340
|
+
if scope:
|
|
341
|
+
queryset = queryset.filter(scope=scope)
|
|
342
|
+
|
|
343
|
+
return list(queryset)
|
|
344
|
+
|
|
345
|
+
except Exception as exc:
|
|
346
|
+
self.exception_handler(exc, self.logger)
|
|
347
|
+
|
|
348
|
+
def get_user_roles(self, user: AbstractBaseUser) -> list[str]:
|
|
349
|
+
"""
|
|
350
|
+
Récupère tous les rôles uniques assignés à un utilisateur.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
user: L'utilisateur dont on récupère les rôles
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Liste des slugs de rôles
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
role_slugs = Grant.objects.filter(
|
|
360
|
+
user=user,
|
|
361
|
+
role__isnull=False
|
|
362
|
+
).values_list('role__slug', flat=True).distinct()
|
|
363
|
+
|
|
364
|
+
return list(role_slugs)
|
|
365
|
+
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
self.exception_handler(exc, self.logger)
|
|
368
|
+
|
|
369
|
+
def create_role(self, slug: str, name: str) -> Role:
|
|
370
|
+
"""
|
|
371
|
+
Crée un nouveau rôle.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
slug: Identifiant unique du rôle
|
|
375
|
+
name: Nom du rôle
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Le rôle créé
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
DuplicateEntryException: Si le rôle existe déjà
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
role = Role.objects.create(slug=slug, name=name)
|
|
385
|
+
return role
|
|
386
|
+
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
self.exception_handler(exc, self.logger)
|
|
389
|
+
|
|
390
|
+
def create_group(
|
|
391
|
+
self,
|
|
392
|
+
group_data
|
|
393
|
+
) -> Group:
|
|
394
|
+
"""
|
|
395
|
+
Crée un nouveau groupe et lui assigne des rôles.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
slug: Identifiant unique du groupe
|
|
399
|
+
name: Nom du groupe
|
|
400
|
+
role_slugs: Liste optionnelle des slugs de rôles à assigner
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Le groupe créé
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
DuplicateEntryException: Si le groupe existe déjà
|
|
407
|
+
NotFoundException: Si un rôle n'existe pas
|
|
408
|
+
"""
|
|
409
|
+
try:
|
|
410
|
+
group = Group.objects.create(slug=group_data.slug, name=group_data.name)
|
|
411
|
+
|
|
412
|
+
if group_data.roles:
|
|
413
|
+
roles = Role.objects.filter(slug__in=group_data.roles)
|
|
414
|
+
|
|
415
|
+
if roles.count() != len(group_data.roles):
|
|
416
|
+
found_slugs = set(roles.values_list('slug', flat=True))
|
|
417
|
+
missing_slugs = set(group_data.roles) - found_slugs
|
|
418
|
+
raise RoleNotFoundException(
|
|
419
|
+
detail=f"Rôles non trouvés: {list(missing_slugs)}"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
group.roles.set(roles)
|
|
423
|
+
|
|
424
|
+
logger.info("group_created", slug=group_data.slug, name=group_data.name, role_slugs=group_data.roles, role_count=len(group_data.roles) if group_data.roles else 0)
|
|
425
|
+
return group
|
|
426
|
+
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
self.exception_handler(exc, self.logger)
|
|
429
|
+
|
|
430
|
+
@transaction.atomic
|
|
431
|
+
def create_role_grant(
|
|
432
|
+
self,
|
|
433
|
+
grant_data
|
|
434
|
+
) -> RoleGrant:
|
|
435
|
+
"""
|
|
436
|
+
Crée un role grant (template de permissions pour un rôle).
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
role_slug: Slug du rôle
|
|
440
|
+
scope: Scope du grant
|
|
441
|
+
actions: Liste des actions autorisées
|
|
442
|
+
context: Contexte JSON optionnel
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Le role grant créé
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
NotFoundException: Si le rôle n'existe pas
|
|
449
|
+
DuplicateEntryException: Si le role grant existe déjà
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
role = Role.objects.get(slug=grant_data.role)
|
|
453
|
+
|
|
454
|
+
role_grant = RoleGrant.objects.create(
|
|
455
|
+
role=role,
|
|
456
|
+
scope=grant_data.scope,
|
|
457
|
+
actions=grant_data.actions,
|
|
458
|
+
context=grant_data.context
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
logger.info("role_grant_created", role_slug=grant_data.role, scope=grant_data.scope, actions=grant_data.actions)
|
|
462
|
+
|
|
463
|
+
return role_grant
|
|
464
|
+
|
|
465
|
+
except Role.DoesNotExist:
|
|
466
|
+
raise RoleNotFoundException(detail=f"Le rôle '{grant_data.role}' n'existe pas")
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
self.exception_handler(exc, self.logger)
|
|
469
|
+
|
|
470
|
+
def get_role_grants(self, role_slug: str) -> list[RoleGrant]:
|
|
471
|
+
"""
|
|
472
|
+
Récupère tous les grants d'un rôle.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
role_slug: Slug du rôle
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Liste des role grants
|
|
479
|
+
|
|
480
|
+
Raises:
|
|
481
|
+
NotFoundException: Si le rôle n'existe pas
|
|
482
|
+
"""
|
|
483
|
+
try:
|
|
484
|
+
role = Role.objects.get(slug=role_slug)
|
|
485
|
+
return list(RoleGrant.objects.filter(role=role))
|
|
486
|
+
|
|
487
|
+
except Exception as exc:
|
|
488
|
+
self.exception_handler(exc, self.logger)
|
|
489
|
+
|
|
490
|
+
@transaction.atomic
|
|
491
|
+
def create_grant(
|
|
492
|
+
self,
|
|
493
|
+
grant_data
|
|
494
|
+
) -> Grant:
|
|
495
|
+
"""
|
|
496
|
+
Crée un grant personnalisé pour un utilisateur.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
grant_data: Schéma contenant les données du grant
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Le grant créé
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
NotFoundException: Si l'utilisateur ou le rôle n'existe pas
|
|
506
|
+
"""
|
|
507
|
+
try:
|
|
508
|
+
user = User.objects.get(pk=grant_data.user_id)
|
|
509
|
+
|
|
510
|
+
role_obj = None
|
|
511
|
+
if grant_data.role:
|
|
512
|
+
try:
|
|
513
|
+
role_obj = Role.objects.get(slug=grant_data.role)
|
|
514
|
+
except Role.DoesNotExist:
|
|
515
|
+
raise RoleNotFoundException(detail=f"Le rôle '{grant_data.role}' n'existe pas")
|
|
516
|
+
|
|
517
|
+
grant = Grant.objects.create(
|
|
518
|
+
user=user,
|
|
519
|
+
role=role_obj,
|
|
520
|
+
scope=grant_data.scope,
|
|
521
|
+
actions=grant_data.actions,
|
|
522
|
+
context=grant_data.context,
|
|
523
|
+
user_group=None
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
logger.info("grant_created", user_id=grant_data.user_id, scope=grant_data.scope, actions=grant_data.actions)
|
|
527
|
+
|
|
528
|
+
return grant
|
|
529
|
+
|
|
530
|
+
except User.DoesNotExist:
|
|
531
|
+
raise NotFoundException(detail=f"L'utilisateur avec l'ID {grant_data.user_id} n'existe pas")
|
|
532
|
+
except Exception as exc:
|
|
533
|
+
self.exception_handler(exc, self.logger)
|
|
534
|
+
|
|
535
|
+
def update_grant(
|
|
536
|
+
self,
|
|
537
|
+
grant_id: int,
|
|
538
|
+
grant_data
|
|
539
|
+
) -> Grant:
|
|
540
|
+
"""
|
|
541
|
+
Met à jour un grant existant.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
grant_id: ID du grant à mettre à jour
|
|
545
|
+
grant_data: Schéma contenant les nouvelles données
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Le grant mis à jour
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
GrantNotFoundException: Si le grant n'existe pas
|
|
552
|
+
"""
|
|
553
|
+
try:
|
|
554
|
+
grant = Grant.objects.get(pk=grant_id)
|
|
555
|
+
|
|
556
|
+
if grant_data.actions is not None:
|
|
557
|
+
grant.actions = grant_data.actions
|
|
558
|
+
|
|
559
|
+
if grant_data.context is not None:
|
|
560
|
+
grant.context = grant_data.context
|
|
561
|
+
|
|
562
|
+
if grant_data.role is not None:
|
|
563
|
+
try:
|
|
564
|
+
grant.role = Role.objects.get(slug=grant_data.role)
|
|
565
|
+
except Role.DoesNotExist:
|
|
566
|
+
raise RoleNotFoundException(detail=f"Le rôle '{grant_data.role}' n'existe pas")
|
|
567
|
+
|
|
568
|
+
grant.save()
|
|
569
|
+
|
|
570
|
+
logger.info("grant_updated", grant_id=grant_id)
|
|
571
|
+
|
|
572
|
+
return grant
|
|
573
|
+
|
|
574
|
+
except Grant.DoesNotExist:
|
|
575
|
+
raise GrantNotFoundException(detail=f"Le grant avec l'ID {grant_id} n'existe pas")
|
|
576
|
+
except Exception as exc:
|
|
577
|
+
self.exception_handler(exc, self.logger)
|
|
578
|
+
|
|
579
|
+
def delete_grant(
|
|
580
|
+
self,
|
|
581
|
+
grant_id: int
|
|
582
|
+
) -> None:
|
|
583
|
+
"""
|
|
584
|
+
Supprime un grant.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
grant_id: ID du grant à supprimer
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
GrantNotFoundException: Si le grant n'existe pas
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
grant = Grant.objects.get(pk=grant_id)
|
|
594
|
+
grant.delete()
|
|
595
|
+
|
|
596
|
+
logger.info("grant_deleted", grant_id=grant_id)
|
|
597
|
+
|
|
598
|
+
except Grant.DoesNotExist:
|
|
599
|
+
raise GrantNotFoundException(detail=f"Le grant avec l'ID {grant_id} n'existe pas")
|
|
600
|
+
except Exception as exc:
|
|
601
|
+
self.exception_handler(exc, self.logger)
|
|
602
|
+
|
|
603
|
+
def update_role_grant(
|
|
604
|
+
self,
|
|
605
|
+
grant_id: int,
|
|
606
|
+
grant_data
|
|
607
|
+
) -> RoleGrant:
|
|
608
|
+
"""
|
|
609
|
+
Met à jour un role grant existant.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
grant_id: ID du role grant à mettre à jour
|
|
613
|
+
grant_data: Schéma contenant les nouvelles données
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Le role grant mis à jour
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
RoleGrantNotFoundException: Si le role grant n'existe pas
|
|
620
|
+
"""
|
|
621
|
+
try:
|
|
622
|
+
role_grant = RoleGrant.objects.get(pk=grant_id)
|
|
623
|
+
|
|
624
|
+
if grant_data.actions is not None:
|
|
625
|
+
role_grant.actions = grant_data.actions
|
|
626
|
+
|
|
627
|
+
if grant_data.context is not None:
|
|
628
|
+
role_grant.context = grant_data.context
|
|
629
|
+
|
|
630
|
+
role_grant.save()
|
|
631
|
+
|
|
632
|
+
logger.info("role_grant_updated", grant_id=grant_id)
|
|
633
|
+
|
|
634
|
+
return role_grant
|
|
635
|
+
|
|
636
|
+
except RoleGrant.DoesNotExist:
|
|
637
|
+
raise RoleGrantNotFoundException(detail=f"Le role grant avec l'ID {grant_id} n'existe pas")
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
self.exception_handler(exc, self.logger)
|
|
640
|
+
|
|
641
|
+
def delete_role_grant(
|
|
642
|
+
self,
|
|
643
|
+
grant_id: int
|
|
644
|
+
) -> None:
|
|
645
|
+
"""
|
|
646
|
+
Supprime un role grant.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
grant_id: ID du role grant à supprimer
|
|
650
|
+
|
|
651
|
+
Raises:
|
|
652
|
+
RoleGrantNotFoundException: Si le role grant n'existe pas
|
|
653
|
+
"""
|
|
654
|
+
try:
|
|
655
|
+
role_grant = RoleGrant.objects.get(pk=grant_id)
|
|
656
|
+
role_grant.delete()
|
|
657
|
+
|
|
658
|
+
logger.info("role_grant_deleted", grant_id=grant_id)
|
|
659
|
+
|
|
660
|
+
except RoleGrant.DoesNotExist:
|
|
661
|
+
raise RoleGrantNotFoundException(detail=f"Le role grant avec l'ID {grant_id} n'existe pas")
|
|
662
|
+
except Exception as exc:
|
|
663
|
+
self.exception_handler(exc, self.logger)
|