oxutils 0.1.7__py3-none-any.whl → 0.1.10__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 (55) hide show
  1. oxutils/__init__.py +2 -1
  2. oxutils/constants.py +4 -0
  3. oxutils/jwt/auth.py +150 -1
  4. oxutils/jwt/models.py +32 -1
  5. oxutils/jwt/utils.py +45 -0
  6. oxutils/logger/receivers.py +10 -6
  7. oxutils/models/base.py +102 -0
  8. oxutils/models/fields.py +79 -0
  9. oxutils/oxiliere/apps.py +6 -1
  10. oxutils/oxiliere/authorization.py +45 -0
  11. oxutils/oxiliere/caches.py +8 -7
  12. oxutils/oxiliere/checks.py +31 -0
  13. oxutils/oxiliere/constants.py +3 -0
  14. oxutils/oxiliere/context.py +18 -0
  15. oxutils/oxiliere/exceptions.py +16 -0
  16. oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
  17. oxutils/oxiliere/management/commands/init_oxiliere_system.py +20 -8
  18. oxutils/oxiliere/middleware.py +29 -13
  19. oxutils/oxiliere/models.py +130 -19
  20. oxutils/oxiliere/permissions.py +6 -5
  21. oxutils/oxiliere/schemas.py +13 -4
  22. oxutils/oxiliere/signals.py +5 -0
  23. oxutils/oxiliere/utils.py +18 -0
  24. oxutils/pagination/__init__.py +0 -0
  25. oxutils/pagination/cursor.py +367 -0
  26. oxutils/permissions/__init__.py +0 -0
  27. oxutils/permissions/actions.py +57 -0
  28. oxutils/permissions/admin.py +3 -0
  29. oxutils/permissions/apps.py +10 -0
  30. oxutils/permissions/caches.py +19 -0
  31. oxutils/permissions/checks.py +188 -0
  32. oxutils/permissions/constants.py +0 -0
  33. oxutils/permissions/controllers.py +344 -0
  34. oxutils/permissions/exceptions.py +60 -0
  35. oxutils/permissions/management/__init__.py +0 -0
  36. oxutils/permissions/management/commands/__init__.py +0 -0
  37. oxutils/permissions/management/commands/load_permission_preset.py +112 -0
  38. oxutils/permissions/migrations/0001_initial.py +112 -0
  39. oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
  40. oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
  41. oxutils/permissions/migrations/__init__.py +0 -0
  42. oxutils/permissions/models.py +171 -0
  43. oxutils/permissions/perms.py +95 -0
  44. oxutils/permissions/queryset.py +92 -0
  45. oxutils/permissions/schemas.py +276 -0
  46. oxutils/permissions/services.py +663 -0
  47. oxutils/permissions/tests.py +3 -0
  48. oxutils/permissions/utils.py +628 -0
  49. oxutils/settings.py +1 -0
  50. oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
  51. oxutils/users/models.py +2 -0
  52. oxutils/utils.py +25 -0
  53. {oxutils-0.1.7.dist-info → oxutils-0.1.10.dist-info}/METADATA +1 -1
  54. {oxutils-0.1.7.dist-info → oxutils-0.1.10.dist-info}/RECORD +55 -19
  55. {oxutils-0.1.7.dist-info → oxutils-0.1.10.dist-info}/WHEEL +0 -0
@@ -0,0 +1,344 @@
1
+ from typing import List, Optional
2
+ from django.conf import settings
3
+ from django.http import HttpRequest
4
+ from ninja_extra import (
5
+ api_controller,
6
+ ControllerBase,
7
+ http_get,
8
+ http_post,
9
+ http_put,
10
+ http_delete,
11
+ paginate,
12
+ )
13
+ from ninja_extra.permissions import IsAuthenticated
14
+ from ninja_extra.pagination import PageNumberPaginationExtra, PaginatedResponseSchema
15
+ from . import schemas
16
+ from .models import Role, Group, RoleGrant, Grant
17
+ from .services import PermissionService
18
+ from .perms import access_manager
19
+ from oxutils.exceptions import NotFoundException, ValidationException
20
+
21
+
22
+
23
+
24
+ @api_controller(
25
+ "/access",
26
+ permissions=[
27
+ IsAuthenticated & access_manager('r')
28
+ ]
29
+ )
30
+ class PermissionController(ControllerBase):
31
+ """
32
+ Contrôleur pour la gestion des permissions, rôles et groupes.
33
+ """
34
+ service = PermissionService()
35
+
36
+ @http_get('/scopes', response=List[str])
37
+ def list_scopes(self):
38
+ return getattr(settings, 'ACCESS_SCOPES', [])
39
+
40
+ @http_get("/roles", response=PaginatedResponseSchema[schemas.RoleSchema])
41
+ @paginate(PageNumberPaginationExtra, page_size=20)
42
+ def list_roles(self):
43
+ """
44
+ Liste tous les rôles.
45
+ """
46
+ return self.service.get_roles()
47
+
48
+ @http_get("/roles/{role_slug}", response=schemas.RoleSchema)
49
+ def get_role(self, role_slug: str):
50
+ """
51
+ Récupère un rôle par son slug.
52
+ """
53
+ return self.service.get_role(role_slug)
54
+
55
+ # Groupes
56
+ @http_post(
57
+ "/groups",
58
+ response=schemas.GroupSchema,
59
+ permissions=[
60
+ IsAuthenticated & access_manager('w')
61
+ ]
62
+ )
63
+ def create_group(self, group_data: schemas.GroupCreateSchema):
64
+ """
65
+ Crée un nouveau groupe de rôles.
66
+ """
67
+ return self.service.create_group(group_data)
68
+
69
+ @http_get(
70
+ "/groups",
71
+ response=PaginatedResponseSchema[schemas.GroupSchema],
72
+ )
73
+ @paginate(PageNumberPaginationExtra, page_size=20)
74
+ def list_groups(self):
75
+ """
76
+ Liste tous les groupes de rôles.
77
+ """
78
+ return Group.objects.all()
79
+
80
+ @http_get(
81
+ "/groups/{group_slug}",
82
+ response=schemas.GroupSchema,
83
+ )
84
+ def get_group(self, group_slug: str):
85
+ """
86
+ Récupère un groupe par son slug.
87
+ """
88
+ try:
89
+ return Group.objects.get(slug=group_slug)
90
+ except Group.DoesNotExist:
91
+ raise NotFoundException("Groupe non trouvé")
92
+
93
+ @http_put(
94
+ "/groups/{group_slug}",
95
+ response=schemas.GroupSchema,
96
+ permissions=[
97
+ IsAuthenticated & access_manager('ru')
98
+ ]
99
+ )
100
+ def update_group(self, group_slug: str, group_data: schemas.GroupUpdateSchema):
101
+ """
102
+ Met à jour un groupe existant.
103
+ """
104
+ try:
105
+ group = Group.objects.get(slug=group_slug)
106
+
107
+ # Mise à jour des champs simples
108
+ for field, value in group_data.dict(exclude_unset=True, exclude={"roles"}).items():
109
+ setattr(group, field, value)
110
+
111
+ # Mise à jour des rôles si fournis
112
+ if group_data.roles is not None:
113
+ roles = Role.objects.filter(slug__in=group_data.roles)
114
+ group.roles.set(roles)
115
+
116
+ group.save()
117
+ return group
118
+ except Group.DoesNotExist:
119
+ raise NotFoundException("Groupe non trouvé")
120
+ except Exception as e:
121
+ raise ValidationException(str(e))
122
+
123
+ @http_delete(
124
+ "/groups/{group_slug}",
125
+ response={
126
+ "204": None
127
+ },
128
+ permissions=[
129
+ IsAuthenticated & access_manager('d')
130
+ ]
131
+ )
132
+ def delete_group(self, group_slug: str):
133
+ """
134
+ Supprime un groupe.
135
+ """
136
+ try:
137
+ group = Group.objects.get(slug=group_slug)
138
+ group.delete()
139
+ return None
140
+ except Group.DoesNotExist:
141
+ raise NotFoundException("Groupe non trouvé")
142
+
143
+ # Rôles des utilisateurs
144
+ @http_post(
145
+ "/users/assign-role",
146
+ response=schemas.RoleSchema,
147
+ permissions=[
148
+ IsAuthenticated & access_manager('rw')
149
+ ]
150
+ )
151
+ def assign_role_to_user(self, data: schemas.AssignRoleSchema, request: HttpRequest):
152
+ """
153
+ Assigne un rôle à un utilisateur.
154
+ """
155
+ return self.service.assign_role_to_user(
156
+ user_id=data.user_id,
157
+ role_slug=data.role,
158
+ by_user=request.user if request.user.is_authenticated else None
159
+ )
160
+
161
+ @http_post(
162
+ "/users/revoke-role",
163
+ response={
164
+ "204": None
165
+ },
166
+ permissions=[
167
+ IsAuthenticated & access_manager('rw')
168
+ ]
169
+ )
170
+ def revoke_role_from_user(self, data: schemas.RevokeRoleSchema):
171
+ """
172
+ Révoque un rôle d'un utilisateur.
173
+ """
174
+ self.service.revoke_role_from_user(
175
+ user_id=data.user_id,
176
+ role_slug=data.role
177
+ )
178
+ return None
179
+
180
+ @http_post(
181
+ "/users/assign-group",
182
+ response=List[schemas.RoleSchema],
183
+ permissions=[
184
+ IsAuthenticated & access_manager('rw')
185
+ ]
186
+ )
187
+ def assign_group_to_user(self, data: schemas.AssignGroupSchema, request: HttpRequest):
188
+ """
189
+ Assigne un groupe de rôles à un utilisateur.
190
+ """
191
+ return self.service.assign_group_to_user(
192
+ user_id=data.user_id,
193
+ group_slug=data.group,
194
+ by_user=request.user if request.user.is_authenticated else None
195
+ )
196
+
197
+ @http_post(
198
+ "/users/revoke-group",
199
+ response={
200
+ "204": None
201
+ },
202
+ permissions=[
203
+ IsAuthenticated & access_manager('rw')
204
+ ]
205
+ )
206
+ def revoke_group_from_user(self, data: schemas.RevokeGroupSchema):
207
+ """
208
+ Révoque un groupe de rôles d'un utilisateur.
209
+ """
210
+ self.service.revoke_group_from_user(
211
+ user_id=data.user_id,
212
+ group_slug=data.group
213
+ )
214
+ return None
215
+
216
+ @http_post(
217
+ "/groups/{group_slug}/sync",
218
+ response=schemas.GroupSyncResponseSchema,
219
+ permissions=[
220
+ IsAuthenticated & access_manager('rw')
221
+ ]
222
+ )
223
+ def sync_group(self, group_slug: str):
224
+ """
225
+ Synchronise les grants de tous les utilisateurs d'un groupe.
226
+ À appeler après modification des RoleGrants ou des rôles du groupe.
227
+ """
228
+ return self.service.sync_group(group_slug)
229
+
230
+ # Grants
231
+ @http_post(
232
+ "/grants",
233
+ response=schemas.GrantSchema,
234
+ permissions=[
235
+ IsAuthenticated & access_manager('rw')
236
+ ]
237
+ )
238
+ def create_grant(self, grant_data: schemas.GrantCreateSchema):
239
+ """
240
+ Crée une nouvelle permission personnalisée pour un utilisateur.
241
+ """
242
+ return self.service.create_grant(grant_data)
243
+
244
+ @http_get(
245
+ "/grants",
246
+ response=PaginatedResponseSchema[schemas.GrantSchema],
247
+ )
248
+ @paginate(PageNumberPaginationExtra, page_size=20)
249
+ def list_grants(self, user_id: Optional[int] = None, role: Optional[str] = None):
250
+ """
251
+ Liste les grants, avec filtrage optionnel par utilisateur et/ou rôle.
252
+ """
253
+ queryset = Grant.objects.all()
254
+ if user_id:
255
+ queryset = queryset.filter(user_id=user_id)
256
+ if role:
257
+ queryset = queryset.filter(role__slug=role)
258
+ return queryset
259
+
260
+ @http_put(
261
+ "/grants/{grant_id}",
262
+ response=schemas.GrantSchema,
263
+ permissions=[
264
+ IsAuthenticated & access_manager('ru')
265
+ ]
266
+ )
267
+ def update_grant(self, grant_id: int, grant_data: schemas.GrantUpdateSchema):
268
+ """
269
+ Met à jour une permission personnalisée.
270
+ """
271
+ return self.service.update_grant(grant_id, grant_data)
272
+
273
+ @http_delete(
274
+ "/grants/{grant_id}",
275
+ response={
276
+ "204": None
277
+ },
278
+ permissions=[
279
+ IsAuthenticated & access_manager('d')
280
+ ]
281
+ )
282
+ def delete_grant(self, grant_id: int):
283
+ """
284
+ Supprime une permission personnalisée.
285
+ """
286
+ self.service.delete_grant(grant_id)
287
+ return None
288
+
289
+ # Role Grants
290
+ @http_post(
291
+ "/role-grants",
292
+ response=schemas.RoleGrantSchema,
293
+ permissions=[
294
+ IsAuthenticated & access_manager('rw')
295
+ ]
296
+ )
297
+ def create_role_grant(self, grant_data: schemas.RoleGrantCreateSchema):
298
+ """
299
+ Crée une nouvelle permission pour un rôle.
300
+ """
301
+ return self.service.create_role_grant(grant_data)
302
+
303
+ @http_get(
304
+ "/role-grants",
305
+ response=PaginatedResponseSchema[schemas.RoleGrantSchema],
306
+ )
307
+ @paginate(PageNumberPaginationExtra, page_size=20)
308
+ def list_role_grants(self, role: Optional[str] = None):
309
+ """
310
+ Liste les permissions de rôles, avec filtrage optionnel par rôle.
311
+ """
312
+ queryset = RoleGrant.objects.select_related('role').all()
313
+ if role:
314
+ queryset = queryset.filter(role__slug=role)
315
+ return queryset
316
+
317
+ @http_put(
318
+ "/role-grants/{grant_id}",
319
+ response=schemas.RoleGrantSchema,
320
+ permissions=[
321
+ IsAuthenticated & access_manager('ru')
322
+ ]
323
+ )
324
+ def update_role_grant(self, grant_id: int, grant_data: schemas.RoleGrantUpdateSchema):
325
+ """
326
+ Met à jour une permission de rôle.
327
+ """
328
+ return self.service.update_role_grant(grant_id, grant_data)
329
+
330
+ @http_delete(
331
+ "/role-grants/{grant_id}/",
332
+ response={
333
+ "204": None
334
+ },
335
+ permissions=[
336
+ IsAuthenticated & access_manager('d')
337
+ ]
338
+ )
339
+ def delete_role_grant(self, grant_id: int):
340
+ """
341
+ Supprime une permission de rôle.
342
+ """
343
+ self.service.delete_role_grant(grant_id)
344
+ return None
@@ -0,0 +1,60 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from oxutils.exceptions import (
3
+ APIException,
4
+ NotFoundException,
5
+ ValidationException,
6
+ DuplicateEntryException,
7
+ PermissionDeniedException,
8
+ ExceptionCode
9
+ )
10
+
11
+
12
+ class RoleNotFoundException(NotFoundException):
13
+ """Exception levée quand un rôle n'est pas trouvé."""
14
+ default_detail = _('Le rôle demandé n\'existe pas')
15
+
16
+
17
+ class GroupNotFoundException(NotFoundException):
18
+ """Exception levée quand un groupe n'est pas trouvé."""
19
+ default_detail = _('Le groupe demandé n\'existe pas')
20
+
21
+
22
+ class GrantNotFoundException(NotFoundException):
23
+ """Exception levée quand un grant n'est pas trouvé."""
24
+ default_detail = _('Le grant demandé n\'existe pas')
25
+
26
+
27
+ class RoleGrantNotFoundException(NotFoundException):
28
+ """Exception levée quand un role grant n'est pas trouvé."""
29
+ default_detail = _('Le role grant demandé n\'existe pas')
30
+
31
+
32
+ class RoleAlreadyAssignedException(DuplicateEntryException):
33
+ """Exception levée quand un rôle est déjà assigné à un utilisateur."""
34
+ default_detail = _('Ce rôle est déjà assigné à l\'utilisateur')
35
+
36
+
37
+ class GroupAlreadyAssignedException(DuplicateEntryException):
38
+ """Exception levée quand un groupe est déjà assigné à un utilisateur."""
39
+ default_detail = _('Ce groupe est déjà assigné à l\'utilisateur')
40
+
41
+
42
+ class InvalidActionsException(ValidationException):
43
+ """Exception levée quand des actions invalides sont fournies."""
44
+ default_detail = _('Les actions fournies sont invalides')
45
+
46
+
47
+ class InsufficientPermissionsException(PermissionDeniedException):
48
+ """Exception levée quand l'utilisateur n'a pas les permissions suffisantes."""
49
+ default_code = ExceptionCode.INSUFFICIENT_PERMISSIONS
50
+ default_detail = _('Permissions insuffisantes pour effectuer cette action')
51
+
52
+
53
+ class RoleGrantConflictException(DuplicateEntryException):
54
+ """Exception levée quand un role grant existe déjà pour ce rôle et scope."""
55
+ default_detail = _('Un role grant existe déjà pour ce rôle et ce scope')
56
+
57
+
58
+ class GrantConflictException(DuplicateEntryException):
59
+ """Exception levée quand un grant existe déjà pour cet utilisateur et scope."""
60
+ default_detail = _('Un grant existe déjà pour cet utilisateur et ce scope')
File without changes
File without changes
@@ -0,0 +1,112 @@
1
+ from typing import Any
2
+ from django.core.management.base import BaseCommand, CommandError
3
+ from django.db import transaction
4
+
5
+ from oxutils.permissions.utils import load_preset
6
+
7
+
8
+ class Command(BaseCommand):
9
+ """
10
+ Commande de management Django pour charger un preset de permissions.
11
+
12
+ Usage:
13
+ python manage.py load_permission_preset
14
+ python manage.py load_permission_preset --dry-run
15
+ python manage.py load_permission_preset --force
16
+ python manage.py load_permission_preset --dry-run --force
17
+ """
18
+
19
+ help = "Charge un preset de permissions depuis settings.PERMISSION_PRESET"
20
+
21
+ def add_arguments(self, parser) -> None:
22
+ """
23
+ Ajoute les arguments de la commande.
24
+
25
+ Args:
26
+ parser: ArgumentParser de Django
27
+ """
28
+ parser.add_argument(
29
+ '--dry-run',
30
+ action='store_true',
31
+ help='Simule le chargement sans créer les objets en base de données',
32
+ )
33
+ parser.add_argument(
34
+ '--force',
35
+ action='store_true',
36
+ help='Force le chargement même si des rôles existent déjà en base',
37
+ )
38
+
39
+ @transaction.atomic
40
+ def handle(self, *args: Any, **options: Any) -> None:
41
+ """
42
+ Exécute la commande de chargement du preset.
43
+
44
+ Args:
45
+ *args: Arguments positionnels
46
+ **options: Options de la commande
47
+
48
+ Raises:
49
+ CommandError: Si le preset n'est pas défini ou est invalide
50
+ """
51
+ dry_run = options.get('dry_run', False)
52
+ force = options.get('force', False)
53
+
54
+ if dry_run:
55
+ self.stdout.write(
56
+ self.style.WARNING('Mode DRY-RUN activé - Aucune modification ne sera effectuée')
57
+ )
58
+
59
+ if force:
60
+ self.stdout.write(
61
+ self.style.WARNING('Mode FORCE activé - Les rôles existants seront ignorés')
62
+ )
63
+
64
+ try:
65
+ # Charger le preset
66
+ self.stdout.write('Chargement du preset de permissions...')
67
+
68
+ if dry_run:
69
+ # En mode dry-run, on utilise un savepoint pour rollback
70
+ sid = transaction.savepoint()
71
+
72
+ stats = load_preset(force=force)
73
+
74
+ if dry_run:
75
+ # Rollback en mode dry-run
76
+ transaction.savepoint_rollback(sid)
77
+
78
+ # Afficher les statistiques
79
+ self.stdout.write(
80
+ self.style.SUCCESS('\n✓ Preset chargé avec succès!')
81
+ )
82
+ self.stdout.write(f" • Rôles créés: {stats['roles']}")
83
+ self.stdout.write(f" • Groupes créés: {stats['groups']}")
84
+ self.stdout.write(f" • Role grants créés: {stats['role_grants']}")
85
+
86
+ if dry_run:
87
+ self.stdout.write(
88
+ self.style.WARNING('\nAucune modification effectuée (mode dry-run)')
89
+ )
90
+
91
+ except AttributeError as e:
92
+ raise CommandError(
93
+ f"Erreur de configuration: {str(e)}\n"
94
+ "Assurez-vous que PERMISSION_PRESET est défini dans vos settings Django."
95
+ )
96
+
97
+ except PermissionError as e:
98
+ raise CommandError(
99
+ f"{str(e)}\n"
100
+ "Utilisez --force pour forcer le chargement malgré les rôles existants."
101
+ )
102
+
103
+ except (KeyError, ValueError) as e:
104
+ raise CommandError(
105
+ f"Erreur dans le preset: {str(e)}\n"
106
+ "Vérifiez la structure de votre PERMISSION_PRESET."
107
+ )
108
+
109
+ except Exception as e:
110
+ raise CommandError(
111
+ f"Erreur inattendue lors du chargement du preset: {str(e)}"
112
+ )
@@ -0,0 +1,112 @@
1
+ # Generated by Django 5.2.9 on 2025-12-27 10:49
2
+
3
+ import django.contrib.postgres.fields
4
+ import django.contrib.postgres.indexes
5
+ import django.db.models.deletion
6
+ from django.conf import settings
7
+ from django.db import migrations, models
8
+
9
+
10
+ class Migration(migrations.Migration):
11
+
12
+ initial = True
13
+
14
+ dependencies = [
15
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name='Role',
21
+ fields=[
22
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='Date and time when this record was created')),
23
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='Date and time when this record was last updated')),
24
+ ('slug', models.SlugField(primary_key=True, serialize=False, unique=True)),
25
+ ('name', models.CharField(max_length=100)),
26
+ ],
27
+ options={
28
+ 'indexes': [models.Index(fields=['slug'], name='permissions_slug_ae4163_idx')],
29
+ },
30
+ ),
31
+ migrations.CreateModel(
32
+ name='Group',
33
+ fields=[
34
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='Date and time when this record was created')),
35
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='Date and time when this record was last updated')),
36
+ ('slug', models.SlugField(primary_key=True, serialize=False, unique=True)),
37
+ ('name', models.CharField(max_length=100)),
38
+ ('roles', models.ManyToManyField(related_name='groups', to='permissions.role')),
39
+ ],
40
+ options={
41
+ 'abstract': False,
42
+ },
43
+ ),
44
+ migrations.CreateModel(
45
+ name='UserGroup',
46
+ fields=[
47
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
48
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='Date and time when this record was created')),
49
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='Date and time when this record was last updated')),
50
+ ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_groups', to='permissions.group')),
51
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_groups', to=settings.AUTH_USER_MODEL)),
52
+ ],
53
+ ),
54
+ migrations.CreateModel(
55
+ name='Grant',
56
+ fields=[
57
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='Date and time when this record was created')),
59
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='Date and time when this record was last updated')),
60
+ ('scope', models.CharField(max_length=100)),
61
+ ('actions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None)),
62
+ ('context', models.JSONField(blank=True, default=dict)),
63
+ ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_grants', to=settings.AUTH_USER_MODEL)),
64
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to=settings.AUTH_USER_MODEL)),
65
+ ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='grants', to='permissions.role')),
66
+ ('user_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='grants', to='permissions.usergroup')),
67
+ ],
68
+ ),
69
+ migrations.CreateModel(
70
+ name='RoleGrant',
71
+ fields=[
72
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
73
+ ('scope', models.CharField(max_length=100)),
74
+ ('actions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None)),
75
+ ('context', models.JSONField(blank=True, default=dict)),
76
+ ('group', models.ForeignKey(blank=True, help_text='Groupe optionnel pour des comportements spécifiques', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='role_grants', to='permissions.group')),
77
+ ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to='permissions.role')),
78
+ ],
79
+ options={
80
+ 'indexes': [models.Index(fields=['role'], name='permissions_role_id_382ed4_idx'), models.Index(fields=['group'], name='permissions_group_i_465f8d_idx'), models.Index(fields=['role', 'group'], name='permissions_role_id_0818de_idx')],
81
+ 'constraints': [models.UniqueConstraint(fields=('role', 'scope', 'group'), name='unique_role_scope_group')],
82
+ },
83
+ ),
84
+ migrations.AddIndex(
85
+ model_name='usergroup',
86
+ index=models.Index(fields=['user', 'group'], name='permissions_user_id_f1ff5d_idx'),
87
+ ),
88
+ migrations.AddConstraint(
89
+ model_name='usergroup',
90
+ constraint=models.UniqueConstraint(fields=('user', 'group'), name='unique_user_group'),
91
+ ),
92
+ migrations.AddIndex(
93
+ model_name='grant',
94
+ index=models.Index(fields=['user', 'scope'], name='permissions_user_id_8a615b_idx'),
95
+ ),
96
+ migrations.AddIndex(
97
+ model_name='grant',
98
+ index=models.Index(fields=['user_group'], name='permissions_user_gr_ec61ff_idx'),
99
+ ),
100
+ migrations.AddIndex(
101
+ model_name='grant',
102
+ index=django.contrib.postgres.indexes.GinIndex(fields=['actions'], name='permissions_actions_541150_gin'),
103
+ ),
104
+ migrations.AddIndex(
105
+ model_name='grant',
106
+ index=django.contrib.postgres.indexes.GinIndex(fields=['context'], name='permissions_context_7b1c0e_gin'),
107
+ ),
108
+ migrations.AddConstraint(
109
+ model_name='grant',
110
+ constraint=models.UniqueConstraint(fields=('user', 'scope', 'role', 'user_group'), name='unique_user_scope_role'),
111
+ ),
112
+ ]
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.9 on 2025-12-29 09:04
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('permissions', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='grant',
16
+ name='role',
17
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_grants', to='permissions.role'),
18
+ ),
19
+ ]
@@ -0,0 +1,33 @@
1
+ # Generated by Django 5.2.9 on 2025-12-29 13:28
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('permissions', '0002_alter_grant_role'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name='grant',
15
+ options={'ordering': ['scope']},
16
+ ),
17
+ migrations.AlterModelOptions(
18
+ name='group',
19
+ options={'ordering': ['slug']},
20
+ ),
21
+ migrations.AlterModelOptions(
22
+ name='role',
23
+ options={'ordering': ['slug']},
24
+ ),
25
+ migrations.AlterModelOptions(
26
+ name='rolegrant',
27
+ options={'ordering': ['role__slug', 'group__slug']},
28
+ ),
29
+ migrations.AddIndex(
30
+ model_name='group',
31
+ index=models.Index(fields=['slug'], name='permissions_slug_ed0901_idx'),
32
+ ),
33
+ ]
File without changes