Projet

Général

Profil

0004-a2_rbac-move-abstract-model-code-from-django_rbac-58.patch

Valentin Deniaud, 04 octobre 2022 16:33

Télécharger (21,1 ko)

Voir les différences:

Subject: [PATCH 4/4] a2_rbac: move abstract model code from django_rbac
 (#58696)

 src/authentic2/a2_rbac/models.py              | 199 +++++++++++++-
 .../migrations/0009_auto_20221004_1343.py     |  53 ++++
 src/django_rbac/models.py                     | 244 +-----------------
 3 files changed, 240 insertions(+), 256 deletions(-)
 create mode 100644 src/django_rbac/migrations/0009_auto_20221004_1343.py
src/authentic2/a2_rbac/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import hashlib
17 18
import os
18 19
from collections import namedtuple
19 20

  
21
from django.conf import settings
22
from django.contrib.auth import get_user_model
20 23
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
21 24
from django.contrib.contenttypes.models import ContentType
22 25
from django.core.exceptions import ValidationError
23 26
from django.core.validators import MinValueValidator
24 27
from django.db import models
28
from django.db.models.query import Prefetch, Q
25 29
from django.urls import reverse
26 30
from django.utils.text import slugify
31
from django.utils.translation import gettext
27 32
from django.utils.translation import gettext_lazy as _
28 33
from django.utils.translation import pgettext_lazy
34
from model_utils.managers import QueryManager
29 35

  
30 36
from authentic2.decorators import errorcollector
31 37
from authentic2.utils.cache import GlobalCache
32 38
from authentic2.validators import HexaColourValidator
39
from django_rbac import managers as rbac_managers
33 40
from django_rbac import utils as rbac_utils
34
from django_rbac.models import (
35
    VIEW_OP,
36
    Operation,
37
    OrganizationalUnitAbstractBase,
38
    PermissionAbstractBase,
39
    RoleAbstractBase,
40
    RoleParentingAbstractBase,
41
)
41
from django_rbac.models import VIEW_OP, Operation
42 42

  
43 43
from . import app_settings, fields, managers
44 44

  
45 45

  
46
class OrganizationalUnit(OrganizationalUnitAbstractBase):
46
class AbstractBase(models.Model):
47
    """Abstract base model for all models having a name and uuid and a
48
    slug
49
    """
50

  
51
    uuid = models.CharField(
52
        max_length=32, verbose_name=_('uuid'), unique=True, default=rbac_utils.get_hex_uuid
53
    )
54
    name = models.CharField(max_length=256, verbose_name=_('name'))
55
    slug = models.SlugField(max_length=256, verbose_name=_('slug'))
56
    description = models.TextField(verbose_name=_('description'), blank=True)
57

  
58
    objects = rbac_managers.AbstractBaseManager()
59

  
60
    def __str__(self):
61
        return str(self.name)
62

  
63
    def __repr__(self):
64
        return f'<{self.__class__.__name__} {repr(self.slug)} {repr(self.name)}>'
65

  
66
    def save(self, *args, **kwargs):
67
        # truncate slug and add a hash if it's too long
68
        if not self.slug:
69
            self.slug = rbac_utils.generate_slug(self.name)
70
        if len(self.slug) > 256:
71
            self.slug = self.slug[:252] + hashlib.md5(self.slug).hexdigest()[:4]
72
        if not self.uuid:
73
            self.uuid = rbac_utils.get_hex_uuid()
74
        return super().save(*args, **kwargs)
75

  
76
    def natural_key(self):
77
        return [self.uuid]
78

  
79
    class Meta:
80
        abstract = True
81

  
82

  
83
class OrganizationalUnit(AbstractBase):
47 84

  
48 85
    RESET_LINK_POLICY = 0
49 86
    MANUAL_PASSWORD_POLICY = 1
......
131 168
            ('slug',),
132 169
        )
133 170

  
171
    def as_scope(self):
172
        return self
173

  
134 174
    def clean(self):
135 175
        # if we set this ou as the default one, we must unset the other one if
136 176
        # there is
......
176 216
            os.unlink(self.logo.path)
177 217

  
178 218
        Permission.objects.filter(ou=self).delete()
179
        return super(OrganizationalUnitAbstractBase, self).delete(*args, **kwargs)
219
        return super().delete(*args, **kwargs)
180 220

  
181 221
    def natural_key(self):
182 222
        return [self.slug]
......
208 248
OrganizationalUnit._meta.natural_key = [['uuid'], ['slug'], ['name']]
209 249

  
210 250

  
211
class Permission(PermissionAbstractBase):
251
class Permission(models.Model):
252
    operation = models.ForeignKey(to=Operation, verbose_name=_('operation'), on_delete=models.CASCADE)
253
    ou = models.ForeignKey(
254
        to=rbac_utils.get_ou_model_name(),
255
        verbose_name=_('organizational unit'),
256
        related_name='scoped_permission',
257
        null=True,
258
        on_delete=models.CASCADE,
259
    )
260
    target_ct = models.ForeignKey(to='contenttypes.ContentType', related_name='+', on_delete=models.CASCADE)
261
    target_id = models.PositiveIntegerField()
262
    target = GenericForeignKey('target_ct', 'target_id')
263

  
264
    objects = rbac_managers.PermissionManager()
265

  
212 266
    class Meta:
213 267
        verbose_name = _('permission')
214 268
        verbose_name_plural = _('permissions')
......
226 280
        object_id_field='admin_scope_id',
227 281
    )
228 282

  
283
    def natural_key(self):
284
        return [
285
            self.operation.slug,
286
            self.ou and self.ou.natural_key(),
287
            self.target and self.target_ct.natural_key(),
288
            self.target and self.target.natural_key(),
289
        ]
290

  
291
    def export_json(self):
292
        return {
293
            "operation": self.operation.natural_key_json(),
294
            "ou": self.ou and self.ou.natural_key_json(),
295
            'target_ct': self.target_ct.natural_key_json(),
296
            "target": self.target.natural_key_json(),
297
        }
298

  
299
    def __str__(self):
300
        ct = ContentType.objects.get_for_id(self.target_ct_id)
301
        ct_ct = ContentType.objects.get_for_model(ContentType)
302
        if ct == ct_ct:
303
            target = ContentType.objects.get_for_id(self.target_id)
304
            s = f'{self.operation} / {target}'
305
        else:
306
            s = f'{self.operation} / {ct.name} / {self.target}'
307
        if self.ou:
308
            s += gettext(' (scope "{0}")').format(self.ou)
309
        return s
310

  
229 311

  
230 312
Permission._meta.natural_key = [
231 313
    ['operation', 'ou', 'target'],
......
233 315
]
234 316

  
235 317

  
236
class Role(RoleAbstractBase):
318
class Role(AbstractBase):
319
    ou = models.ForeignKey(
320
        to=rbac_utils.get_ou_model_name(),
321
        verbose_name=_('organizational unit'),
322
        swappable=True,
323
        blank=True,
324
        null=True,
325
        on_delete=models.CASCADE,
326
    )
327
    members = models.ManyToManyField(
328
        to=settings.AUTH_USER_MODEL, swappable=True, blank=True, related_name='roles'
329
    )
330
    permissions = models.ManyToManyField(
331
        to=rbac_utils.get_permission_model_name(), related_name='roles', blank=True
332
    )
237 333
    name = models.TextField(verbose_name=_('name'))
238 334
    admin_scope_ct = models.ForeignKey(
239 335
        to='contenttypes.ContentType',
......
262 358
        default=True, verbose_name=_('Allow adding or deleting role members')
263 359
    )
264 360

  
361
    objects = rbac_managers.RoleQuerySet.as_manager()
362

  
363
    def add_child(self, child):
364
        RoleParenting = rbac_utils.get_role_parenting_model()
365
        RoleParenting.objects.soft_create(self, child)
366

  
367
    def remove_child(self, child):
368
        RoleParenting = rbac_utils.get_role_parenting_model()
369
        RoleParenting.objects.soft_delete(self, child)
370

  
371
    def add_parent(self, parent):
372
        RoleParenting = rbac_utils.get_role_parenting_model()
373
        RoleParenting.objects.soft_create(parent, self)
374

  
375
    def remove_parent(self, parent):
376
        RoleParenting = rbac_utils.get_role_parenting_model()
377
        RoleParenting.objects.soft_delete(parent, self)
378

  
379
    def parents(self, include_self=True, annotate=False, direct=None):
380
        return self.__class__.objects.filter(pk=self.pk).parents(
381
            include_self=include_self, annotate=annotate, direct=direct
382
        )
383

  
384
    def children(self, include_self=True, annotate=False, direct=None):
385
        return self.__class__.objects.filter(pk=self.pk).children(
386
            include_self=include_self,
387
            annotate=annotate,
388
            direct=direct,
389
        )
390

  
391
    def all_members(self):
392
        User = get_user_model()
393
        prefetch = Prefetch('roles', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='direct')
394

  
395
        return (
396
            User.objects.filter(
397
                Q(roles=self)
398
                | Q(roles__parent_relation__parent=self) & Q(roles__parent_relation__deleted__isnull=True)
399
            )
400
            .distinct()
401
            .prefetch_related(prefetch)
402
        )
403

  
404
    def is_direct(self):
405
        if hasattr(self, 'direct'):
406
            if self.direct is None:
407
                return True
408
            return bool(self.direct)
409
        return None
410

  
265 411
    def get_admin_role(self, create=True):
266 412
        from . import utils
267 413

  
......
503 649
]
504 650

  
505 651

  
506
class RoleParenting(RoleParentingAbstractBase):
507
    class Meta(RoleParentingAbstractBase.Meta):
652
class RoleParenting(models.Model):
653
    parent = models.ForeignKey(
654
        to=rbac_utils.get_role_model_name(),
655
        swappable=True,
656
        related_name='child_relation',
657
        on_delete=models.CASCADE,
658
    )
659
    child = models.ForeignKey(
660
        to=rbac_utils.get_role_model_name(),
661
        swappable=True,
662
        related_name='parent_relation',
663
        on_delete=models.CASCADE,
664
    )
665
    direct = models.BooleanField(default=True, blank=True)
666
    created = models.DateTimeField(verbose_name=_('Creation date'), auto_now_add=True)
667
    deleted = models.DateTimeField(verbose_name=_('Deletion date'), null=True)
668

  
669
    objects = rbac_managers.RoleParentingManager()
670
    alive = QueryManager(deleted__isnull=True)
671

  
672
    def natural_key(self):
673
        return [self.parent.natural_key(), self.child.natural_key(), self.direct]
674

  
675
    class Meta:
508 676
        verbose_name = _('role parenting relation')
509 677
        verbose_name_plural = _('role parenting relations')
678
        unique_together = (('parent', 'child', 'direct'),)
679
        # covering indexes
680
        index_together = (('child', 'parent', 'direct'),)
510 681

  
511 682
    def __str__(self):
512 683
        return '{} {}> {}'.format(self.parent.name, '-' if self.direct else '~', self.child.name)
src/django_rbac/migrations/0009_auto_20221004_1343.py
1
# Generated by Django 2.2.26 on 2022-10-04 11:43
2

  
3
from django.db import migrations
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('django_rbac', '0008_add_roleparenting_soft_delete'),
10
    ]
11

  
12
    operations = [
13
        migrations.DeleteModel(
14
            name='OrganizationalUnit',
15
        ),
16
        migrations.RemoveField(
17
            model_name='role',
18
            name='members',
19
        ),
20
        migrations.RemoveField(
21
            model_name='role',
22
            name='ou',
23
        ),
24
        migrations.RemoveField(
25
            model_name='role',
26
            name='permissions',
27
        ),
28
        migrations.AlterUniqueTogether(
29
            name='roleparenting',
30
            unique_together=None,
31
        ),
32
        migrations.AlterIndexTogether(
33
            name='roleparenting',
34
            index_together=None,
35
        ),
36
        migrations.RemoveField(
37
            model_name='roleparenting',
38
            name='child',
39
        ),
40
        migrations.RemoveField(
41
            model_name='roleparenting',
42
            name='parent',
43
        ),
44
        migrations.DeleteModel(
45
            name='Permission',
46
        ),
47
        migrations.DeleteModel(
48
            name='Role',
49
        ),
50
        migrations.DeleteModel(
51
            name='RoleParenting',
52
        ),
53
    ]
src/django_rbac/models.py
1 1
import functools
2
import hashlib
3 2
import operator
4 3

  
5
from django.conf import settings
6 4
from django.contrib import auth
7
from django.contrib.auth import get_user_model
8 5
from django.contrib.auth.models import Group
9 6
from django.contrib.auth.models import Permission as AuthPermission
10 7
from django.contrib.auth.models import _user_has_module_perms, _user_has_perm
......
18 15
        return _user_get_permissions(user, obj, 'all')
19 16

  
20 17

  
21
from django.contrib.contenttypes.fields import GenericForeignKey
22
from django.contrib.contenttypes.models import ContentType
23 18
from django.db import models
24
from django.db.models.query import Prefetch, Q
25
from django.utils.translation import gettext, pgettext_lazy
19
from django.utils.translation import pgettext_lazy
26 20
from django.utils.translation import ugettext_lazy as _
27
from model_utils.managers import QueryManager
28 21

  
29
from . import backends, constants, managers, utils
30

  
31

  
32
class AbstractBase(models.Model):
33
    """Abstract base model for all models having a name and uuid and a
34
    slug
35
    """
36

  
37
    uuid = models.CharField(max_length=32, verbose_name=_('uuid'), unique=True, default=utils.get_hex_uuid)
38
    name = models.CharField(max_length=256, verbose_name=_('name'))
39
    slug = models.SlugField(max_length=256, verbose_name=_('slug'))
40
    description = models.TextField(verbose_name=_('description'), blank=True)
41

  
42
    objects = managers.AbstractBaseManager()
43

  
44
    def __str__(self):
45
        return str(self.name)
46

  
47
    def __repr__(self):
48
        return f'<{self.__class__.__name__} {repr(self.slug)} {repr(self.name)}>'
49

  
50
    def save(self, *args, **kwargs):
51
        # truncate slug and add a hash if it's too long
52
        if not self.slug:
53
            self.slug = utils.generate_slug(self.name)
54
        if len(self.slug) > 256:
55
            self.slug = self.slug[:252] + hashlib.md5(self.slug).hexdigest()[:4]
56
        if not self.uuid:
57
            self.uuid = utils.get_hex_uuid()
58
        return super().save(*args, **kwargs)
59

  
60
    def natural_key(self):
61
        return [self.uuid]
62

  
63
    class Meta:
64
        abstract = True
65

  
66

  
67
class AbstractOrganizationalUnitScopedBase(models.Model):
68
    '''Base abstract model class for model needing to be scoped by ou'''
69

  
70
    ou = models.ForeignKey(
71
        to=utils.get_ou_model_name(),
72
        verbose_name=_('organizational unit'),
73
        swappable=True,
74
        blank=True,
75
        null=True,
76
        on_delete=models.CASCADE,
77
    )
78

  
79
    class Meta:
80
        abstract = True
81

  
82

  
83
class OrganizationalUnitAbstractBase(AbstractBase):
84
    class Meta:
85
        abstract = True
86

  
87
    def as_scope(self):
88
        """When used as scope to find permissions. Can return a queryset
89
        in a swapped model if for example your OU are hierarchical.
90

  
91
        Must return an OrganizationalUnit or a queryset.
92
        """
93
        return self
94

  
95

  
96
class OrganizationalUnit(OrganizationalUnitAbstractBase):
97
    class Meta(OrganizationalUnitAbstractBase.Meta):
98
        verbose_name = _('organizational unit')
99
        verbose_name_plural = _('organizational units')
100
        swappable = constants.RBAC_OU_MODEL_SETTING
22
from . import backends, managers
101 23

  
102 24

  
103 25
class Operation(models.Model):
......
129 51
Operation._meta.natural_key = ['slug']
130 52

  
131 53

  
132
class PermissionAbstractBase(models.Model):
133
    operation = models.ForeignKey(to=Operation, verbose_name=_('operation'), on_delete=models.CASCADE)
134
    ou = models.ForeignKey(
135
        to=utils.get_ou_model_name(),
136
        verbose_name=_('organizational unit'),
137
        related_name='scoped_permission',
138
        null=True,
139
        on_delete=models.CASCADE,
140
    )
141
    target_ct = models.ForeignKey(to='contenttypes.ContentType', related_name='+', on_delete=models.CASCADE)
142
    target_id = models.PositiveIntegerField()
143
    target = GenericForeignKey('target_ct', 'target_id')
144

  
145
    objects = managers.PermissionManager()
146

  
147
    def natural_key(self):
148
        return [
149
            self.operation.slug,
150
            self.ou and self.ou.natural_key(),
151
            self.target and self.target_ct.natural_key(),
152
            self.target and self.target.natural_key(),
153
        ]
154

  
155
    def export_json(self):
156
        return {
157
            "operation": self.operation.natural_key_json(),
158
            "ou": self.ou and self.ou.natural_key_json(),
159
            'target_ct': self.target_ct.natural_key_json(),
160
            "target": self.target.natural_key_json(),
161
        }
162

  
163
    def __str__(self):
164
        ct = ContentType.objects.get_for_id(self.target_ct_id)
165
        ct_ct = ContentType.objects.get_for_model(ContentType)
166
        if ct == ct_ct:
167
            target = ContentType.objects.get_for_id(self.target_id)
168
            s = f'{self.operation} / {target}'
169
        else:
170
            s = f'{self.operation} / {ct.name} / {self.target}'
171
        if self.ou:
172
            s += gettext(' (scope "{0}")').format(self.ou)
173
        return s
174

  
175
    class Meta:
176
        abstract = True
177
        # FIXME: it's still allow non-unique permission with ou=null
178
        unique_together = (('operation', 'ou', 'target_ct', 'target_id'),)
179

  
180

  
181
class Permission(PermissionAbstractBase):
182
    class Meta(PermissionAbstractBase.Meta):
183
        swappable = constants.RBAC_PERMISSION_MODEL_SETTING
184
        verbose_name = _('permission')
185
        verbose_name_plural = _('permissions')
186

  
187

  
188
class RoleAbstractBase(AbstractOrganizationalUnitScopedBase, AbstractBase):
189
    members = models.ManyToManyField(
190
        to=settings.AUTH_USER_MODEL, swappable=True, blank=True, related_name='roles'
191
    )
192
    permissions = models.ManyToManyField(
193
        to=utils.get_permission_model_name(), related_name='roles', blank=True
194
    )
195

  
196
    objects = managers.RoleQuerySet.as_manager()
197

  
198
    def add_child(self, child):
199
        RoleParenting = utils.get_role_parenting_model()
200
        RoleParenting.objects.soft_create(self, child)
201

  
202
    def remove_child(self, child):
203
        RoleParenting = utils.get_role_parenting_model()
204
        RoleParenting.objects.soft_delete(self, child)
205

  
206
    def add_parent(self, parent):
207
        RoleParenting = utils.get_role_parenting_model()
208
        RoleParenting.objects.soft_create(parent, self)
209

  
210
    def remove_parent(self, parent):
211
        RoleParenting = utils.get_role_parenting_model()
212
        RoleParenting.objects.soft_delete(parent, self)
213

  
214
    def parents(self, include_self=True, annotate=False, direct=None):
215
        return self.__class__.objects.filter(pk=self.pk).parents(
216
            include_self=include_self, annotate=annotate, direct=direct
217
        )
218

  
219
    def children(self, include_self=True, annotate=False, direct=None):
220
        return self.__class__.objects.filter(pk=self.pk).children(
221
            include_self=include_self,
222
            annotate=annotate,
223
            direct=direct,
224
        )
225

  
226
    def all_members(self):
227
        User = get_user_model()
228
        prefetch = Prefetch('roles', queryset=self.__class__.objects.filter(pk=self.pk), to_attr='direct')
229

  
230
        return (
231
            User.objects.filter(
232
                Q(roles=self)
233
                | Q(roles__parent_relation__parent=self) & Q(roles__parent_relation__deleted__isnull=True)
234
            )
235
            .distinct()
236
            .prefetch_related(prefetch)
237
        )
238

  
239
    def is_direct(self):
240
        if hasattr(self, 'direct'):
241
            if self.direct is None:
242
                return True
243
            return bool(self.direct)
244
        return None
245

  
246
    class Meta:
247
        abstract = True
248

  
249

  
250
class Role(RoleAbstractBase):
251
    class Meta(RoleAbstractBase.Meta):
252
        verbose_name = _('role')
253
        verbose_name_plural = _('roles')
254
        swappable = constants.RBAC_ROLE_MODEL_SETTING
255

  
256

  
257
class RoleParentingAbstractBase(models.Model):
258
    parent = models.ForeignKey(
259
        to=utils.get_role_model_name(),
260
        swappable=True,
261
        related_name='child_relation',
262
        on_delete=models.CASCADE,
263
    )
264
    child = models.ForeignKey(
265
        to=utils.get_role_model_name(),
266
        swappable=True,
267
        related_name='parent_relation',
268
        on_delete=models.CASCADE,
269
    )
270
    direct = models.BooleanField(default=True, blank=True)
271
    created = models.DateTimeField(verbose_name=_('Creation date'), auto_now_add=True)
272
    deleted = models.DateTimeField(verbose_name=_('Deletion date'), null=True)
273

  
274
    objects = managers.RoleParentingManager()
275
    alive = QueryManager(deleted__isnull=True)
276

  
277
    def natural_key(self):
278
        return [self.parent.natural_key(), self.child.natural_key(), self.direct]
279

  
280
    class Meta:
281
        abstract = True
282
        unique_together = (('parent', 'child', 'direct'),)
283
        # covering indexes
284
        index_together = (('child', 'parent', 'direct'),)
285

  
286

  
287
class RoleParenting(RoleParentingAbstractBase):
288
    class Meta(RoleParentingAbstractBase.Meta):
289
        verbose_name = _('role parenting relation')
290
        verbose_name_plural = _('role parenting relations')
291
        swappable = constants.RBAC_ROLE_PARENTING_MODEL_SETTING
292

  
293

  
294 54
class PermissionMixin(models.Model):
295 55
    """
296 56
    A mixin class that adds the fields and methods necessary to support
297
-