oxutils 0.1.6__py3-none-any.whl → 0.1.12__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 (67) hide show
  1. oxutils/__init__.py +2 -2
  2. oxutils/audit/migrations/0001_initial.py +2 -2
  3. oxutils/audit/models.py +2 -2
  4. oxutils/constants.py +6 -0
  5. oxutils/jwt/auth.py +150 -1
  6. oxutils/jwt/models.py +81 -0
  7. oxutils/jwt/tokens.py +69 -0
  8. oxutils/jwt/utils.py +45 -0
  9. oxutils/logger/__init__.py +10 -0
  10. oxutils/logger/receivers.py +10 -6
  11. oxutils/logger/settings.py +2 -2
  12. oxutils/models/base.py +102 -0
  13. oxutils/models/fields.py +79 -0
  14. oxutils/oxiliere/apps.py +9 -1
  15. oxutils/oxiliere/authorization.py +45 -0
  16. oxutils/oxiliere/caches.py +13 -11
  17. oxutils/oxiliere/checks.py +31 -0
  18. oxutils/oxiliere/constants.py +3 -0
  19. oxutils/oxiliere/context.py +16 -0
  20. oxutils/oxiliere/exceptions.py +16 -0
  21. oxutils/oxiliere/management/commands/grant_tenant_owners.py +19 -0
  22. oxutils/oxiliere/management/commands/init_oxiliere_system.py +30 -11
  23. oxutils/oxiliere/middleware.py +65 -11
  24. oxutils/oxiliere/models.py +146 -9
  25. oxutils/oxiliere/permissions.py +28 -35
  26. oxutils/oxiliere/schemas.py +16 -6
  27. oxutils/oxiliere/signals.py +5 -0
  28. oxutils/oxiliere/utils.py +36 -1
  29. oxutils/pagination/cursor.py +367 -0
  30. oxutils/permissions/__init__.py +0 -0
  31. oxutils/permissions/actions.py +57 -0
  32. oxutils/permissions/admin.py +3 -0
  33. oxutils/permissions/apps.py +10 -0
  34. oxutils/permissions/caches.py +19 -0
  35. oxutils/permissions/checks.py +188 -0
  36. oxutils/permissions/constants.py +0 -0
  37. oxutils/permissions/controllers.py +344 -0
  38. oxutils/permissions/exceptions.py +60 -0
  39. oxutils/permissions/management/__init__.py +0 -0
  40. oxutils/permissions/management/commands/__init__.py +0 -0
  41. oxutils/permissions/management/commands/load_permission_preset.py +112 -0
  42. oxutils/permissions/migrations/0001_initial.py +112 -0
  43. oxutils/permissions/migrations/0002_alter_grant_role.py +19 -0
  44. oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +33 -0
  45. oxutils/permissions/migrations/__init__.py +0 -0
  46. oxutils/permissions/models.py +171 -0
  47. oxutils/permissions/perms.py +95 -0
  48. oxutils/permissions/queryset.py +92 -0
  49. oxutils/permissions/schemas.py +276 -0
  50. oxutils/permissions/services.py +663 -0
  51. oxutils/permissions/tests.py +3 -0
  52. oxutils/permissions/utils.py +628 -0
  53. oxutils/settings.py +14 -194
  54. oxutils/users/apps.py +1 -1
  55. oxutils/users/migrations/0001_initial.py +47 -0
  56. oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +23 -0
  57. oxutils/users/models.py +2 -0
  58. oxutils/utils.py +25 -0
  59. {oxutils-0.1.6.dist-info → oxutils-0.1.12.dist-info}/METADATA +14 -11
  60. oxutils-0.1.12.dist-info/RECORD +122 -0
  61. oxutils/jwt/client.py +0 -123
  62. oxutils/jwt/constants.py +0 -1
  63. oxutils/s3/settings.py +0 -34
  64. oxutils/s3/storages.py +0 -130
  65. oxutils-0.1.6.dist-info/RECORD +0 -88
  66. /oxutils/{s3 → pagination}/__init__.py +0 -0
  67. {oxutils-0.1.6.dist-info → oxutils-0.1.12.dist-info}/WHEEL +0 -0
@@ -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
@@ -0,0 +1,171 @@
1
+ from django.conf import settings
2
+ from django.db import models
3
+ from django.contrib.postgres.fields import ArrayField
4
+ from django.contrib.postgres.indexes import GinIndex
5
+ from oxutils.models import TimestampMixin
6
+ from .actions import expand_actions
7
+
8
+
9
+
10
+
11
+ class Role(TimestampMixin):
12
+ """
13
+ A role.
14
+ """
15
+ slug = models.SlugField(unique=True, primary_key=True)
16
+ name = models.CharField(max_length=100)
17
+
18
+ def __str__(self):
19
+ return self.slug
20
+
21
+ class Meta:
22
+ indexes = [
23
+ models.Index(fields=["slug"]),
24
+ ]
25
+ ordering = ["slug"]
26
+
27
+
28
+ class Group(TimestampMixin):
29
+ """
30
+ A group of roles. for UI Template purposes.
31
+ """
32
+ slug = models.SlugField(unique=True, primary_key=True)
33
+ name = models.CharField(max_length=100)
34
+ roles = models.ManyToManyField(Role, related_name="groups")
35
+
36
+ def __str__(self):
37
+ return self.slug
38
+
39
+ class Meta:
40
+ indexes = [
41
+ models.Index(fields=["slug"]),
42
+ ]
43
+ ordering = ["slug"]
44
+
45
+
46
+ class UserGroup(TimestampMixin):
47
+ """
48
+ A user group that links users to groups.
49
+ """
50
+ user = models.ForeignKey(
51
+ settings.AUTH_USER_MODEL,
52
+ on_delete=models.CASCADE,
53
+ related_name="user_groups",
54
+ )
55
+ group = models.ForeignKey(
56
+ Group,
57
+ on_delete=models.CASCADE,
58
+ related_name="user_groups",
59
+ )
60
+
61
+ class Meta:
62
+ constraints = [
63
+ models.UniqueConstraint(fields=['user', 'group'], name='unique_user_group')
64
+ ]
65
+ indexes = [
66
+ models.Index(fields=['user', 'group']),
67
+ ]
68
+
69
+
70
+ class RoleGrant(models.Model):
71
+ """
72
+ A grant template of permissions to a role.
73
+ Peut être lié à un groupe spécifique pour des comportements distincts.
74
+ """
75
+ role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="grants")
76
+ group = models.ForeignKey(
77
+ Group,
78
+ null=True,
79
+ blank=True,
80
+ on_delete=models.CASCADE,
81
+ related_name="role_grants",
82
+ help_text="Groupe optionnel pour des comportements spécifiques"
83
+ )
84
+
85
+ scope = models.CharField(max_length=100)
86
+ actions = ArrayField(models.CharField(max_length=5))
87
+ context = models.JSONField(default=dict, blank=True)
88
+
89
+ def clean(self):
90
+ self.actions = expand_actions(self.actions)
91
+
92
+ class Meta:
93
+ constraints = [
94
+ models.UniqueConstraint(
95
+ fields=["role", "scope", "group"], name="unique_role_scope_group"
96
+ )
97
+ ]
98
+ indexes = [
99
+ models.Index(fields=["role"]),
100
+ models.Index(fields=["group"]),
101
+ models.Index(fields=["role", "group"]),
102
+ ]
103
+ ordering = ["role__slug", "group__slug"]
104
+
105
+ def __str__(self):
106
+ group_str = f"[{self.group.slug}]" if self.group else ""
107
+ return f"{self.role}:{self.scope}{group_str}:{self.actions}"
108
+
109
+
110
+ def save(self, *args, **kwargs):
111
+ self.clean()
112
+ super().save(*args, **kwargs)
113
+
114
+
115
+ class Grant(TimestampMixin):
116
+ """
117
+ A grant of permissions to a user.
118
+ """
119
+ user = models.ForeignKey(
120
+ settings.AUTH_USER_MODEL,
121
+ on_delete=models.CASCADE,
122
+ related_name="grants",
123
+ )
124
+
125
+ # traçabilité
126
+ role = models.ForeignKey(
127
+ Role,
128
+ null=True,
129
+ blank=True,
130
+ related_name="user_grants",
131
+ on_delete=models.SET_NULL,
132
+ )
133
+
134
+ # Lien avec UserGroup pour tracer l'origine du grant
135
+ user_group = models.ForeignKey(
136
+ 'UserGroup',
137
+ null=True,
138
+ blank=True,
139
+ related_name="grants",
140
+ on_delete=models.SET_NULL,
141
+ )
142
+
143
+ created_by = models.ForeignKey(
144
+ settings.AUTH_USER_MODEL,
145
+ null=True,
146
+ blank=True,
147
+ related_name="created_grants",
148
+ on_delete=models.SET_NULL,
149
+ )
150
+
151
+ scope = models.CharField(max_length=100)
152
+ actions = ArrayField(models.CharField(max_length=5))
153
+ context = models.JSONField(default=dict, blank=True)
154
+
155
+
156
+ class Meta:
157
+ constraints = [
158
+ models.UniqueConstraint(
159
+ fields=["user", "scope", "role", "user_group"], name="unique_user_scope_role"
160
+ )
161
+ ]
162
+ indexes = [
163
+ models.Index(fields=["user", "scope"]),
164
+ models.Index(fields=["user_group"]),
165
+ GinIndex(fields=["actions"]),
166
+ GinIndex(fields=["context"]),
167
+ ]
168
+ ordering = ["scope"]
169
+
170
+ def __str__(self):
171
+ return f"{self.user} {self.scope} {self.actions}"
@@ -0,0 +1,95 @@
1
+ from typing import Optional
2
+ from django.conf import settings
3
+ from django.core.exceptions import ImproperlyConfigured
4
+ from django.http import HttpRequest
5
+ from ninja_extra.permissions import BasePermission
6
+ from ninja_extra.controllers import ControllerBase
7
+
8
+ from oxutils.permissions.utils import str_check
9
+
10
+
11
+
12
+ class ScopePermission(BasePermission):
13
+ """
14
+ Permission class for checking user permissions using the string format.
15
+
16
+ Format: "<scope>:<actions>:<group>?key=value"
17
+
18
+ Example:
19
+ @api_controller('/articles', permissions=[ScopePermission('articles:w:staff')])
20
+ class ArticleController:
21
+ pass
22
+ """
23
+
24
+ def __init__(self, perm: str, ctx: Optional[dict] = None):
25
+ """
26
+ Initialize the permission checker.
27
+
28
+ Args:
29
+ perm: Permission string in format "<scope>:<actions>:<group>?context"
30
+ """
31
+ self.perm = perm
32
+ self.ctx = ctx if ctx else dict()
33
+
34
+ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
35
+ """
36
+ Check if the user has the required permission.
37
+
38
+ Args:
39
+ request: HTTP request object
40
+ controller: Controller instance
41
+
42
+ Returns:
43
+ True if user has permission, False otherwise
44
+ """
45
+ return str_check(request.user, self.perm, **self.ctx)
46
+
47
+
48
+ def access_manager(actions: str):
49
+ """
50
+ Factory function for creating ScopePermission instances for access manager.
51
+
52
+ Builds a permission string from settings:
53
+ - ACCESS_MANAGER_SCOPE: The scope to check
54
+ - ACCESS_MANAGER_GROUP: Optional group filter
55
+ - ACCESS_MANAGER_CONTEXT: Optional context dict converted to query params
56
+
57
+ Args:
58
+ actions: Actions required (e.g., 'r', 'rw', 'rwd')
59
+
60
+ Returns:
61
+ ScopePermission instance configured with access manager settings
62
+
63
+ Raises:
64
+ ImproperlyConfigured: If required settings are missing
65
+
66
+ Example:
67
+ @api_controller('/access', permissions=[access_manager('w')])
68
+ class AccessController:
69
+ pass
70
+ """
71
+ # Validate required settings
72
+ if not hasattr(settings, 'ACCESS_MANAGER_SCOPE'):
73
+ raise ImproperlyConfigured(
74
+ 'ACCESS_MANAGER_SCOPE is not defined. '
75
+ 'Add ACCESS_MANAGER_SCOPE = "access" to your settings.'
76
+ )
77
+
78
+ # Build base permission string: scope:actions
79
+ perm = f"{settings.ACCESS_MANAGER_SCOPE}:{actions}"
80
+
81
+ # Add group if defined and not None
82
+ if hasattr(settings, 'ACCESS_MANAGER_GROUP') and settings.ACCESS_MANAGER_GROUP is not None:
83
+ perm += f":{settings.ACCESS_MANAGER_GROUP}"
84
+
85
+ # Get context if defined and not empty
86
+ context = {}
87
+ if hasattr(settings, 'ACCESS_MANAGER_CONTEXT') and settings.ACCESS_MANAGER_CONTEXT:
88
+ context = settings.ACCESS_MANAGER_CONTEXT
89
+ if not isinstance(context, dict):
90
+ raise ImproperlyConfigured(
91
+ 'ACCESS_MANAGER_CONTEXT must be a dictionary. '
92
+ f'Got {type(context).__name__} instead.'
93
+ )
94
+
95
+ return ScopePermission(perm, context)
@@ -0,0 +1,92 @@
1
+ from typing import Any
2
+ from django.db import models
3
+ from django.db.models import Q
4
+ from django.contrib.auth.models import AbstractBaseUser
5
+
6
+ from .models import Grant
7
+
8
+
9
+ class PermissionQuerySet(models.QuerySet):
10
+ """
11
+ QuerySet personnalisé pour filtrer des objets selon les permissions d'un utilisateur.
12
+ Permet de filtrer des querysets en fonction des grants de permissions.
13
+ """
14
+
15
+ def allowed_for(
16
+ self,
17
+ user: AbstractBaseUser,
18
+ scope: str,
19
+ required_actions: list[str],
20
+ **context: Any
21
+ ) -> "PermissionQuerySet":
22
+ """
23
+ Filtre les objets si l'utilisateur a les permissions requises.
24
+ Vérifie l'existence d'un grant valide avant de retourner le queryset.
25
+
26
+ Args:
27
+ user: L'utilisateur dont on vérifie les permissions
28
+ scope: Le scope à vérifier (ex: 'articles', 'users')
29
+ required_actions: Liste des actions requises (ex: ['r'], ['w', 'r'])
30
+ **context: Contexte additionnel pour filtrer (ex: tenant_id=123)
31
+
32
+ Returns:
33
+ QuerySet complet si autorisé, QuerySet vide sinon
34
+
35
+ Example:
36
+ >>> Article.objects.allowed_for(user, 'articles', ['r'])
37
+ >>> Article.objects.allowed_for(user, 'articles', ['w'], tenant_id=123)
38
+ """
39
+ # Construire le filtre pour vérifier l'existence d'un grant
40
+ grant_filter = Q(
41
+ user__pk=user.pk,
42
+ scope=scope,
43
+ actions__contains=list(required_actions),
44
+ )
45
+
46
+ # Ajouter les filtres de contexte si fournis
47
+ if context:
48
+ grant_filter &= Q(context__contains=context)
49
+
50
+ # Si un grant existe, retourner le queryset complet, sinon vide
51
+ if Grant.objects.filter(grant_filter).exists():
52
+ return self
53
+ return self.none()
54
+
55
+ def denied_for(
56
+ self,
57
+ user: AbstractBaseUser,
58
+ scope: str,
59
+ required_actions: list[str],
60
+ **context: Any
61
+ ) -> "PermissionQuerySet":
62
+ """
63
+ Filtre les objets si l'utilisateur N'A PAS les permissions requises.
64
+ Inverse de allowed_for.
65
+
66
+ Args:
67
+ user: L'utilisateur dont on vérifie les permissions
68
+ scope: Le scope à vérifier
69
+ required_actions: Liste des actions requises
70
+ **context: Contexte additionnel pour filtrer
71
+
72
+ Returns:
73
+ QuerySet complet si NON autorisé, QuerySet vide si autorisé
74
+
75
+ Example:
76
+ >>> Article.objects.denied_for(user, 'articles', ['w'])
77
+ """
78
+ # Construire le filtre pour vérifier l'existence d'un grant
79
+ grant_filter = Q(
80
+ user__pk=user.pk,
81
+ scope=scope,
82
+ actions__contains=list(required_actions),
83
+ )
84
+
85
+ # Ajouter les filtres de contexte si fournis
86
+ if context:
87
+ grant_filter &= Q(context__contains=context)
88
+
89
+ # Si un grant existe, retourner vide, sinon le queryset complet
90
+ if Grant.objects.filter(grant_filter).exists():
91
+ return self.none()
92
+ return self