From 5650c3328a836accd5a02ce08354ee2e9c26337a Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Mon, 6 Dec 2021 16:04:55 +0100
Subject: [PATCH 2/3] rbac: use an Operation model from a2_rbac (#58829)

---
 src/authentic2/a2_rbac/apps.py                |   2 +
 src/authentic2/a2_rbac/managers.py            |  87 ++++++++++++-
 .../migrations/0024_fix_self_admin_perm.py    |   3 +-
 .../migrations/0026_auto_20211206_1604.py     |  31 +++++
 src/authentic2/a2_rbac/models.py              | 117 +++++++++++++++---
 src/authentic2/a2_rbac/signal_handlers.py     |  19 ++-
 src/authentic2/a2_rbac/utils.py               |  17 +--
 src/authentic2/data_transfer.py               |  10 +-
 .../management/commands/check-and-repair.py   |   4 +-
 src/authentic2/manager/forms.py               |   3 +-
 tests/test_a2_rbac.py                         |   2 +-
 tests/test_api.py                             |   2 +-
 tests/test_commands.py                        |  14 ++-
 tests/test_manager.py                         |   6 +-
 tests/test_user_manager.py                    |   5 +-
 15 files changed, 274 insertions(+), 48 deletions(-)
 create mode 100644 src/authentic2/a2_rbac/migrations/0026_auto_20211206_1604.py

diff --git a/src/authentic2/a2_rbac/apps.py b/src/authentic2/a2_rbac/apps.py
index f19122af..d02fc170 100644
--- a/src/authentic2/a2_rbac/apps.py
+++ b/src/authentic2/a2_rbac/apps.py
@@ -31,6 +31,8 @@ class Authentic2RBACConfig(AppConfig):
         # update rbac on save to contenttype, ou and roles
         post_save.connect(signal_handlers.update_rbac_on_ou_post_save, sender=models.OrganizationalUnit)
         post_delete.connect(signal_handlers.update_rbac_on_ou_post_delete, sender=models.OrganizationalUnit)
+        # create CRUD operations and admin
+        post_migrate.connect(signal_handlers.create_base_operations, sender=self)
         # keep service role and service ou field in sync
         for subclass in Service.__subclasses__():
             post_save.connect(signal_handlers.update_service_role_ou, sender=subclass)
diff --git a/src/authentic2/a2_rbac/managers.py b/src/authentic2/a2_rbac/managers.py
index 9dbc137a..33acb0bb 100644
--- a/src/authentic2/a2_rbac/managers.py
+++ b/src/authentic2/a2_rbac/managers.py
@@ -15,12 +15,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import Manager, query
+from django.db.models.query import Q
 
 from authentic2.a2_rbac import models
+from authentic2.a2_rbac.utils import get_operation
 from django_rbac.managers import AbstractBaseManager
 from django_rbac.managers import RoleManager as BaseRoleManager
-from django_rbac.models import ADMIN_OP
-from django_rbac.utils import get_operation
 
 
 class OrganizationalUnitManager(AbstractBaseManager):
@@ -28,6 +29,84 @@ class OrganizationalUnitManager(AbstractBaseManager):
         return self.get(slug=uuid)
 
 
+class OperationManager(Manager):
+    def get_by_natural_key(self, slug):
+        return self.get(slug=slug)
+
+    def has_perm(self, user, operation_slug, object_or_model, ou=None):
+        """Test if an user can do the operation given by operation_slug
+        on the given object_or_model eventually scoped by an organizational
+        unit given by ou.
+
+        Returns True or False.
+        """
+        ou_query = Q(ou__isnull=True)
+        if ou:
+            ou_query |= Q(ou=ou.as_scope())
+        ct = ContentType.objects.get_for_model(object_or_model)
+        target_query = Q(target_ct=ContentType.objects.get_for_model(ContentType), target_id=ct.pk)
+        if isinstance(object_or_model, models.Model):
+            target_query |= Q(target_ct=ct, target_id=object.pk)
+        qs = models.Permission.objects.for_user(user)
+        qs = qs.filter(operation__slug=operation_slug)
+        qs = qs.filter(ou_query & target_query)
+        return qs.exists()
+
+
+class PermissionManagerBase(Manager):
+    def get_by_natural_key(self, operation_slug, ou_nk, target_ct, target_nk):
+        qs = self.filter(operation__slug=operation_slug)
+        if ou_nk:
+            try:
+                ou = models.OrganizationalUnit.objects.get_by_natural_key(*ou_nk)
+            except models.OrganizationalUnit.DoesNotExist:
+                raise self.model.DoesNotExist
+            qs = qs.filter(ou=ou)
+        else:
+            qs = qs.filter(ou__isnull=True)
+        try:
+            target_ct = ContentType.objects.get_by_natural_key(*target_ct)
+        except ContentType.DoesNotExist:
+            raise self.model.DoesNotExist
+        target_model = target_ct.model_class()
+        try:
+            target = target_model.objects.get_by_natural_key(*target_nk)
+        except target_model.DoesNotExist:
+            raise self.model.DoesNotExist
+        return qs.get(target_ct=ContentType.objects.get_for_model(target), target_id=target.pk)
+
+
+class PermissionQueryset(query.QuerySet):
+    def by_target_ct(self, target):
+        """Filter permission whose target content-type matches the content
+        type of the target argument
+        """
+        target_ct = ContentType.objects.get_for_model(target)
+        return self.filter(target_ct=target_ct)
+
+    def by_target(self, target):
+        '''Filter permission whose target matches target'''
+        return self.by_target_ct(target).filter(target_id=target.pk)
+
+    def for_user(self, user):
+        """Retrieve all permissions hold by an user through its role and
+        inherited roles.
+        """
+        roles = models.Role.objects.for_user(user=user)
+        return self.filter(roles__in=roles)
+
+    def cleanup(self):
+        count = 0
+        for p in self:
+            if not p.target and (p.target_ct_id or p.target_id):
+                p.delete()
+                count += 1
+        return count
+
+
+PermissionManager = PermissionManagerBase.from_queryset(PermissionQueryset)
+
+
 class RoleManager(BaseRoleManager):
     def get_admin_role(
         self,
@@ -35,7 +114,7 @@ class RoleManager(BaseRoleManager):
         name,
         slug,
         ou=None,
-        operation=ADMIN_OP,
+        operation=None,
         update_name=False,
         update_slug=False,
         permissions=(),
@@ -44,6 +123,8 @@ class RoleManager(BaseRoleManager):
     ):
         '''Get or create the role of manager's of this object instance'''
         kwargs = {}
+        if operation is None:
+            operation = models.ADMIN_OP
         assert not ou or isinstance(
             instance, ContentType
         ), 'get_admin_role(ou=...) can only be used with ContentType instances: %s %s %s' % (
diff --git a/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py b/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py
index b09b4595..4abfe27c 100644
--- a/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py
+++ b/src/authentic2/a2_rbac/migrations/0024_fix_self_admin_perm.py
@@ -2,8 +2,7 @@
 
 from django.db import migrations
 
-from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP
-from django_rbac.models import CHANGE_OP
+from authentic2.a2_rbac.models import CHANGE_OP, MANAGE_MEMBERS_OP
 
 
 def update_self_administration_perm(apps, schema_editor):
diff --git a/src/authentic2/a2_rbac/migrations/0026_auto_20211206_1604.py b/src/authentic2/a2_rbac/migrations/0026_auto_20211206_1604.py
new file mode 100644
index 00000000..8f01d2ed
--- /dev/null
+++ b/src/authentic2/a2_rbac/migrations/0026_auto_20211206_1604.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.2.19 on 2021-12-06 15:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('a2_rbac', '0025_auto_20210622_1132'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Operation',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+                ),
+                ('slug', models.CharField(max_length=32, unique=True, verbose_name='slug')),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='permission',
+            name='operation',
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to='a2_rbac.Operation', verbose_name='operation'
+            ),
+        ),
+    ]
diff --git a/src/authentic2/a2_rbac/models.py b/src/authentic2/a2_rbac/models.py
index b1a252aa..280c92c0 100644
--- a/src/authentic2/a2_rbac/models.py
+++ b/src/authentic2/a2_rbac/models.py
@@ -22,24 +22,18 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
 from django.db import models
 from django.utils.text import slugify
-from django.utils.translation import pgettext_lazy
+from django.utils.translation import gettext, pgettext_lazy
 from django.utils.translation import ugettext_lazy as _
 
+from authentic2.a2_rbac.utils import get_operation
 from authentic2.decorators import GlobalCache, errorcollector
-from django_rbac import utils as rbac_utils
-from django_rbac.models import (
-    VIEW_OP,
-    Operation,
-    OrganizationalUnitAbstractBase,
-    PermissionAbstractBase,
-    RoleAbstractBase,
-    RoleParentingAbstractBase,
-)
+from django_rbac.models import OrganizationalUnitAbstractBase, RoleAbstractBase, RoleParentingAbstractBase
 
-from . import app_settings, fields, managers
+from . import app_settings, fields
 
 
 class OrganizationalUnit(OrganizationalUnitAbstractBase):
+    from .managers import OrganizationalUnitManager
 
     RESET_LINK_POLICY = 0
     MANUAL_PASSWORD_POLICY = 1
@@ -105,7 +99,7 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase):
         blank=True,
     )
 
-    objects = managers.OrganizationalUnitManager()
+    objects = OrganizationalUnitManager()
 
     class Meta:
         verbose_name = _('organizational unit')
@@ -187,6 +181,88 @@ class OrganizationalUnit(OrganizationalUnitAbstractBase):
 OrganizationalUnit._meta.natural_key = [['uuid'], ['slug'], ['name']]
 
 
+class Operation(models.Model):
+    from .managers import OperationManager
+
+    slug = models.CharField(max_length=32, verbose_name=_('slug'), unique=True)
+
+    def natural_key(self):
+        return [self.slug]
+
+    def __str__(self):
+        return str(self._registry.get(self.slug, self.slug))
+
+    def export_json(self):
+        return {'slug': self.slug}
+
+    @property
+    def name(self):
+        return str(self)
+
+    @classmethod
+    def register(cls, name, slug):
+        cls._registry[slug] = name
+        return cls(slug=slug)
+
+    _registry = {}
+
+    objects = OperationManager()
+
+
+Operation._meta.natural_key = ['slug']
+
+
+class PermissionAbstractBase(models.Model):
+    from .managers import PermissionManager
+
+    operation = models.ForeignKey(to=Operation, verbose_name=_('operation'), on_delete=models.CASCADE)
+    ou = models.ForeignKey(
+        to=OrganizationalUnit,
+        verbose_name=_('organizational unit'),
+        related_name='scoped_permission',
+        null=True,
+        on_delete=models.CASCADE,
+    )
+    target_ct = models.ForeignKey(to='contenttypes.ContentType', related_name='+', on_delete=models.CASCADE)
+    target_id = models.PositiveIntegerField()
+    target = GenericForeignKey('target_ct', 'target_id')
+
+    objects = PermissionManager()
+
+    def natural_key(self):
+        return [
+            self.operation.slug,
+            self.ou and self.ou.natural_key(),
+            self.target and self.target_ct.natural_key(),
+            self.target and self.target.natural_key(),
+        ]
+
+    def export_json(self):
+        return {
+            "operation": self.operation.natural_key_json(),
+            "ou": self.ou and self.ou.natural_key_json(),
+            'target_ct': self.target_ct.natural_key_json(),
+            "target": self.target.natural_key_json(),
+        }
+
+    def __str__(self):
+        ct = ContentType.objects.get_for_id(self.target_ct_id)
+        ct_ct = ContentType.objects.get_for_model(ContentType)
+        if ct == ct_ct:
+            target = ContentType.objects.get_for_id(self.target_id)
+            s = f'{self.operation} / {target}'
+        else:
+            s = f'{self.operation} / {ct} / {self.target}'
+        if self.ou:
+            s += gettext(' (scope "{0}")').format(self.ou)
+        return s
+
+    class Meta:
+        abstract = True
+        # FIXME: it's still allow non-unique permission with ou=null
+        unique_together = (('operation', 'ou', 'target_ct', 'target_id'),)
+
+
 class Permission(PermissionAbstractBase):
     class Meta:
         verbose_name = _('permission')
@@ -206,6 +282,8 @@ Permission._meta.natural_key = [
 
 
 class Role(RoleAbstractBase):
+    from .managers import RoleManager
+
     admin_scope_ct = models.ForeignKey(
         to='contenttypes.ContentType',
         null=True,
@@ -290,7 +368,7 @@ class Role(RoleAbstractBase):
     def has_self_administration(self, op=None):
         if not op:
             op = MANAGE_MEMBERS_OP
-        operation = rbac_utils.get_operation(op)
+        operation = get_operation(op)
         self_perm, dummy = Permission.objects.get_or_create(
             operation=operation,
             target_ct=ContentType.objects.get_for_model(self),
@@ -303,7 +381,7 @@ class Role(RoleAbstractBase):
         'Add permission to role so that it is self-administered'
         if not op:
             op = MANAGE_MEMBERS_OP
-        operation = rbac_utils.get_operation(op)
+        operation = get_operation(op)
         self_perm, dummy = Permission.objects.get_or_create(
             operation=operation, target_ct=ContentType.objects.get_for_model(self), target_id=self.pk
         )
@@ -313,7 +391,7 @@ class Role(RoleAbstractBase):
     def is_internal(self):
         return self.slug.startswith('_')
 
-    objects = managers.RoleManager()
+    objects = RoleManager()
 
     class Meta:
         verbose_name = _('role')
@@ -413,6 +491,15 @@ GenericRelation(Permission, content_type_field='target_ct', object_id_field='tar
 )
 
 
+# base rbac operations
+ADMIN_OP = Operation.register(name=_('Management'), slug='admin')
+CHANGE_OP = Operation.register(name=_('Change'), slug='change')
+DELETE_OP = Operation.register(name=_('Delete'), slug='delete')
+ADD_OP = Operation.register(name=_('Add'), slug='add')
+VIEW_OP = Operation.register(name=_('View'), slug='view')
+SEARCH_OP = Operation.register(name=_('Search'), slug='search')
+
+# a2 specific operations
 CHANGE_PASSWORD_OP = Operation.register(name=_('Change password'), slug='change_password')
 RESET_PASSWORD_OP = Operation.register(name=_('Password reset'), slug='reset_password')
 ACTIVATE_OP = Operation.register(name=_('Activation'), slug='activate')
diff --git a/src/authentic2/a2_rbac/signal_handlers.py b/src/authentic2/a2_rbac/signal_handlers.py
index 2c83828e..3030d34f 100644
--- a/src/authentic2/a2_rbac/signal_handlers.py
+++ b/src/authentic2/a2_rbac/signal_handlers.py
@@ -20,10 +20,10 @@ from django.db import DEFAULT_DB_ALIAS, router, transaction
 from django.utils.translation import override
 from django.utils.translation import ugettext as _
 
-from authentic2.a2_rbac.models import OrganizationalUnit, Role
+from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Role
+from authentic2.a2_rbac.utils import get_operation
 from authentic2.utils.misc import get_fk_model
 from django_rbac.managers import defer_update_transitive_closure
-from django_rbac.utils import get_operation
 
 
 def create_default_ou(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
@@ -83,6 +83,21 @@ def update_service_role_ou(sender, instance, created, raw, **kwargs):
     Role.objects.filter(service=instance).update(ou=instance.ou)
 
 
+def create_base_operations(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
+    '''Create some basic operations, matching permissions from Django'''
+    from .models import ADD_OP, ADMIN_OP, CHANGE_OP, DELETE_OP, SEARCH_OP, VIEW_OP
+
+    if not router.allow_migrate(using, Operation):
+        return
+
+    get_operation(ADD_OP)
+    get_operation(CHANGE_OP)
+    get_operation(DELETE_OP)
+    get_operation(VIEW_OP)
+    get_operation(ADMIN_OP)
+    get_operation(SEARCH_OP)
+
+
 def create_default_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
     from .models import (
         ACTIVATE_OP,
diff --git a/src/authentic2/a2_rbac/utils.py b/src/authentic2/a2_rbac/utils.py
index 2f7b786f..ec7acfe7 100644
--- a/src/authentic2/a2_rbac/utils.py
+++ b/src/authentic2/a2_rbac/utils.py
@@ -18,9 +18,6 @@ from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
 from django.utils.text import slugify
 
-from django_rbac import utils as rbac_utils
-from django_rbac.models import SEARCH_OP, VIEW_OP
-
 from . import models
 
 
@@ -34,7 +31,7 @@ def get_default_ou():
 def get_view_user_perm(ou=None):
     User = get_user_model()
     view_user_perm, dummy = models.Permission.objects.get_or_create(
-        operation=rbac_utils.get_operation(VIEW_OP),
+        operation=get_operation(models.VIEW_OP),
         target_ct=ContentType.objects.get_for_model(ContentType),
         target_id=ContentType.objects.get_for_model(User).pk,
         ou__isnull=ou is None,
@@ -46,14 +43,14 @@ def get_view_user_perm(ou=None):
 def get_search_ou_perm(ou=None):
     if ou:
         view_ou_perm, dummy = models.Permission.objects.get_or_create(
-            operation=rbac_utils.get_operation(SEARCH_OP),
+            operation=get_operation(models.SEARCH_OP),
             target_ct=ContentType.objects.get_for_model(ou),
             target_id=ou.pk,
             ou__isnull=True,
         )
     else:
         view_ou_perm, dummy = models.Permission.objects.get_or_create(
-            operation=rbac_utils.get_operation(SEARCH_OP),
+            operation=get_operation(models.SEARCH_OP),
             target_ct=ContentType.objects.get_for_model(ContentType),
             target_id=ContentType.objects.get_for_model(models.OrganizationalUnit).pk,
             ou__isnull=True,
@@ -64,7 +61,7 @@ def get_search_ou_perm(ou=None):
 def get_manage_authorizations_user_perm(ou=None):
     User = get_user_model()
     manage_authorizations_user_perm, dummy = models.Permission.objects.get_or_create(
-        operation=rbac_utils.get_operation(models.MANAGE_AUTHORIZATIONS_OP),
+        operation=get_operation(models.MANAGE_AUTHORIZATIONS_OP),
         target_ct=ContentType.objects.get_for_model(ContentType),
         target_id=ContentType.objects.get_for_model(User).pk,
         ou__isnull=ou is None,
@@ -73,6 +70,12 @@ def get_manage_authorizations_user_perm(ou=None):
     return manage_authorizations_user_perm
 
 
+def get_operation(operation_tpl):
+
+    operation, dummy = models.Operation.objects.get_or_create(slug=operation_tpl.slug)
+    return operation
+
+
 def generate_slug(name, seen_slugs=None):
     slug = base_slug = slugify(name).lstrip('_')
     if seen_slugs:
diff --git a/src/authentic2/data_transfer.py b/src/authentic2/data_transfer.py
index 4ef5d672..ec363c00 100644
--- a/src/authentic2/data_transfer.py
+++ b/src/authentic2/data_transfer.py
@@ -24,10 +24,16 @@ from django.core.validators import validate_slug
 from django.utils.text import format_lazy
 from django.utils.translation import ugettext_lazy as _
 
-from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role, RoleAttribute, RoleParenting
+from authentic2.a2_rbac.models import (
+    Operation,
+    OrganizationalUnit,
+    Permission,
+    Role,
+    RoleAttribute,
+    RoleParenting,
+)
 from authentic2.decorators import errorcollector
 from authentic2.utils.lazy import lazy_join
-from django_rbac.models import Operation
 
 
 def update_model(obj, d):
diff --git a/src/authentic2/management/commands/check-and-repair.py b/src/authentic2/management/commands/check-and-repair.py
index 45770e13..e6b5a565 100644
--- a/src/authentic2/management/commands/check-and-repair.py
+++ b/src/authentic2/management/commands/check-and-repair.py
@@ -29,11 +29,11 @@ from django.db.transaction import atomic
 from django.utils.timezone import localtime
 
 from authentic2 import app_settings
+from authentic2.a2_rbac.models import ADMIN_OP
 from authentic2.a2_rbac.models import OrganizationalUnit as OU
 from authentic2.a2_rbac.models import Permission, Role
+from authentic2.a2_rbac.utils import get_operation
 from authentic2.custom_user.models import User
-from django_rbac.models import ADMIN_OP
-from django_rbac.utils import get_operation
 
 try:
     from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP  # pylint: disable=C0412
diff --git a/src/authentic2/manager/forms.py b/src/authentic2/manager/forms.py
index 781df100..24be0acf 100644
--- a/src/authentic2/manager/forms.py
+++ b/src/authentic2/manager/forms.py
@@ -30,7 +30,7 @@ from django.utils.text import slugify
 from django.utils.translation import pgettext, ugettext
 from django.utils.translation import ugettext_lazy as _
 
-from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role
+from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Permission, Role
 from authentic2.a2_rbac.utils import generate_slug, get_default_ou
 from authentic2.forms.fields import CheckPasswordField, NewPasswordField, ValidatedEmailField
 from authentic2.forms.profile import BaseUserForm
@@ -43,7 +43,6 @@ from authentic2.utils.misc import (
     send_templated_mail,
 )
 from django_rbac.backends import DjangoRBACBackend
-from django_rbac.models import Operation
 
 from . import app_settings, fields, utils
 
diff --git a/tests/test_a2_rbac.py b/tests/test_a2_rbac.py
index 8be28e66..3009888c 100644
--- a/tests/test_a2_rbac.py
+++ b/tests/test_a2_rbac.py
@@ -19,13 +19,13 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.management import call_command
 
+from authentic2.a2_rbac.models import Operation
 from authentic2.a2_rbac.models import OrganizationalUnit as OU
 from authentic2.a2_rbac.models import Permission, Role, RoleAttribute
 from authentic2.a2_rbac.utils import get_default_ou
 from authentic2.custom_user.models import User
 from authentic2.models import Service
 from authentic2.utils.misc import get_hex_uuid
-from django_rbac.models import Operation
 
 from .utils import login, request_select2
 
diff --git a/tests/test_api.py b/tests/test_api.py
index aebf95e6..581bfa3a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -33,13 +33,13 @@ from django.utils.text import slugify
 from requests.models import Response
 from rest_framework import VERSION as drf_version
 
+from authentic2.a2_rbac.models import SEARCH_OP
 from authentic2.a2_rbac.models import OrganizationalUnit as OU
 from authentic2.a2_rbac.models import Role
 from authentic2.a2_rbac.utils import get_default_ou
 from authentic2.apps.journal.models import Event, EventType
 from authentic2.models import Attribute, AttributeValue, AuthorizedRole, PasswordReset, Service
 from authentic2.utils.misc import good_next_url
-from django_rbac.models import SEARCH_OP
 
 from .utils import assert_event, basic_authorization_header, get_link_from_mail, login
 
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 18de7244..11c47f77 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -26,13 +26,19 @@ from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
 from django.utils.timezone import now
 
-from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP, VIEW_OP, OrganizationalUnit, Permission, Role
-from authentic2.a2_rbac.utils import get_default_ou
+from authentic2.a2_rbac.models import (
+    ADMIN_OP,
+    MANAGE_MEMBERS_OP,
+    VIEW_OP,
+    Operation,
+    OrganizationalUnit,
+    Permission,
+    Role,
+)
+from authentic2.a2_rbac.utils import get_default_ou, get_operation
 from authentic2.custom_user.models import DeletedUser
 from authentic2.models import UserExternalId
 from authentic2_auth_oidc.models import OIDCAccount, OIDCProvider
-from django_rbac.models import ADMIN_OP, Operation
-from django_rbac.utils import get_operation
 
 from .utils import call_command, login
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index c241af61..71a8c500 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -27,15 +27,13 @@ from django.urls import reverse
 from django.utils.encoding import force_bytes, force_str
 from webtest import Upload
 
-from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP
+from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP, VIEW_OP
 from authentic2.a2_rbac.models import OrganizationalUnit as OU
 from authentic2.a2_rbac.models import Permission, Role
-from authentic2.a2_rbac.utils import get_default_ou
+from authentic2.a2_rbac.utils import get_default_ou, get_operation
 from authentic2.apps.journal.models import Event
 from authentic2.models import Service
 from authentic2.validators import EmailValidator
-from django_rbac.models import VIEW_OP
-from django_rbac.utils import get_operation
 
 from .utils import assert_event, get_link_from_mail, login, request_select2
 
diff --git a/tests/test_user_manager.py b/tests/test_user_manager.py
index f416a178..439bff75 100644
--- a/tests/test_user_manager.py
+++ b/tests/test_user_manager.py
@@ -27,16 +27,15 @@ from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from webtest import Upload
 
+from authentic2.a2_rbac.models import VIEW_OP
 from authentic2.a2_rbac.models import OrganizationalUnit as OU
 from authentic2.a2_rbac.models import Permission, Role
-from authentic2.a2_rbac.utils import get_default_ou, get_view_user_perm
+from authentic2.a2_rbac.utils import get_default_ou, get_operation, get_view_user_perm
 from authentic2.apps.journal.models import Event
 from authentic2.custom_user.models import User
 from authentic2.manager import user_import
 from authentic2.models import Attribute, AttributeValue
 from authentic2_idp_oidc.models import OIDCAuthorization, OIDCClient
-from django_rbac.models import VIEW_OP
-from django_rbac.utils import get_operation
 
 from .utils import get_link_from_mail, login, logout
 
-- 
2.34.1

