From 29b8e6bf72d063d3edd14c6503cea175b891b6eb Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 11 Oct 2022 16:10:48 +0200 Subject: [PATCH 2/2] a2_rbac: migrate existing operations to new model (#69902) --- .../migrations/0031_new_operation_model.py | 45 ++++++++++++++++ .../migrations/0032_copy_operations_data.py | 43 +++++++++++++++ .../0033_remove_old_operation_fk.py | 30 +++++++++++ src/authentic2/a2_rbac/models.py | 34 +++++++++++- src/authentic2/data_transfer.py | 10 +++- src/authentic2/manager/forms.py | 3 +- .../migrations/0010_delete_operation.py | 17 ++++++ src/django_rbac/models.py | 33 ------------ src/django_rbac/utils.py | 2 +- tests/test_a2_rbac.py | 53 ++++++++++++++++++- tests/test_commands.py | 2 +- tests/test_rbac.py | 3 +- 12 files changed, 231 insertions(+), 44 deletions(-) create mode 100644 src/authentic2/a2_rbac/migrations/0031_new_operation_model.py create mode 100644 src/authentic2/a2_rbac/migrations/0032_copy_operations_data.py create mode 100644 src/authentic2/a2_rbac/migrations/0033_remove_old_operation_fk.py create mode 100644 src/django_rbac/migrations/0010_delete_operation.py delete mode 100644 src/django_rbac/models.py diff --git a/src/authentic2/a2_rbac/migrations/0031_new_operation_model.py b/src/authentic2/a2_rbac/migrations/0031_new_operation_model.py new file mode 100644 index 000000000..56be2e632 --- /dev/null +++ b/src/authentic2/a2_rbac/migrations/0031_new_operation_model.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.26 on 2022-10-11 14:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('a2_rbac', '0030_organizationalunit_min_password_strength'), + ] + + 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.AddField( + model_name='permission', + name='operation_new', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='a2_rbac.Operation', + verbose_name='operation', + ), + preserve_default=False, + ), + migrations.AlterField( + model_name='permission', + name='operation', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='django_rbac.Operation', + verbose_name='operation', + null=True, + ), + ), + ] diff --git a/src/authentic2/a2_rbac/migrations/0032_copy_operations_data.py b/src/authentic2/a2_rbac/migrations/0032_copy_operations_data.py new file mode 100644 index 000000000..f914464f8 --- /dev/null +++ b/src/authentic2/a2_rbac/migrations/0032_copy_operations_data.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.26 on 2022-10-11 13:36 + +from django.db import migrations + + +def copy_operations_data(apps, schema_editor): + OldOperation = apps.get_model('django_rbac', 'Operation') + NewOperation = apps.get_model('a2_rbac', 'Operation') + Permission = apps.get_model('a2_rbac', 'Permission') + + operation_map = {} + for operation in OldOperation.objects.all(): + operation_map[operation.pk] = NewOperation.objects.create(slug=operation.slug) + + for permission in Permission.objects.all(): + permission.operation_new = operation_map[permission.operation_id] + permission.save() + + +def reverse_copy_operations_data(apps, schema_editor): + OldOperation = apps.get_model('django_rbac', 'Operation') + NewOperation = apps.get_model('a2_rbac', 'Operation') + Permission = apps.get_model('a2_rbac', 'Permission') + + operation_map = {} + for operation in NewOperation.objects.all(): + operation_map[operation.pk] = OldOperation.objects.create(slug=operation.slug) + + for permission in Permission.objects.all(): + permission.operation = operation_map[permission.operation_new_id] + permission.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('a2_rbac', '0031_new_operation_model'), + ('django_rbac', '0009_auto_20221004_1343'), + ] + + operations = [ + migrations.RunPython(copy_operations_data, reverse_code=reverse_copy_operations_data), + ] diff --git a/src/authentic2/a2_rbac/migrations/0033_remove_old_operation_fk.py b/src/authentic2/a2_rbac/migrations/0033_remove_old_operation_fk.py new file mode 100644 index 000000000..79f4130ba --- /dev/null +++ b/src/authentic2/a2_rbac/migrations/0033_remove_old_operation_fk.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.26 on 2022-10-11 14:35 + +import django +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('a2_rbac', '0032_copy_operations_data'), + ] + + operations = [ + migrations.RemoveField( + model_name='permission', + name='operation', + ), + migrations.RenameField( + model_name='permission', + old_name='operation_new', + new_name='operation', + ), + 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 8c3d11ca3..7fefc0d26 100644 --- a/src/authentic2/a2_rbac/models.py +++ b/src/authentic2/a2_rbac/models.py @@ -38,7 +38,6 @@ from authentic2.utils.cache import GlobalCache from authentic2.validators import HexaColourValidator from django_rbac import managers as rbac_managers from django_rbac import utils as rbac_utils -from django_rbac.models import Operation from . import app_settings, fields, managers @@ -266,7 +265,9 @@ OrganizationalUnit._meta.natural_key = [['uuid'], ['slug'], ['name']] class Permission(models.Model): - operation = models.ForeignKey(to=Operation, verbose_name=_('operation'), on_delete=models.CASCADE) + operation = models.ForeignKey( + to='a2_rbac.Operation', verbose_name=_('operation'), on_delete=models.CASCADE + ) ou = models.ForeignKey( to=rbac_utils.get_ou_model_name(), verbose_name=_('organizational unit'), @@ -718,6 +719,35 @@ class RoleAttribute(models.Model): return {'name': self.name, 'kind': self.kind, 'value': self.value} +class Operation(models.Model): + 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 = rbac_managers.OperationManager() + + +Operation._meta.natural_key = ['slug'] + + GenericRelation(Permission, content_type_field='target_ct', object_id_field='target_id').contribute_to_class( ContentType, 'admin_perms' ) diff --git a/src/authentic2/data_transfer.py b/src/authentic2/data_transfer.py index e7acc212e..d15517d5f 100644 --- a/src/authentic2/data_transfer.py +++ b/src/authentic2/data_transfer.py @@ -24,11 +24,17 @@ from django.core.validators import validate_slug from django.utils.text import format_lazy from django.utils.translation import gettext_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.a2_rbac.utils import get_default_ou 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/manager/forms.py b/src/authentic2/manager/forms.py index b333b9a64..319c04dbf 100644 --- a/src/authentic2/manager/forms.py +++ b/src/authentic2/manager/forms.py @@ -31,7 +31,7 @@ from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext from django_select2.forms import HeavySelect2Widget -from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role, RoleAttribute +from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Permission, Role, RoleAttribute from authentic2.a2_rbac.utils import generate_slug, get_default_ou from authentic2.custom_user.backends import DjangoRBACBackend from authentic2.forms.fields import ( @@ -46,7 +46,6 @@ from authentic2.models import APIClient, PasswordReset, Service from authentic2.passwords import generate_password, get_min_password_strength from authentic2.utils.misc import send_email_change_email, send_password_reset_mail, send_templated_mail from authentic2.validators import EmailValidator -from django_rbac.models import Operation from . import app_settings, fields, utils diff --git a/src/django_rbac/migrations/0010_delete_operation.py b/src/django_rbac/migrations/0010_delete_operation.py new file mode 100644 index 000000000..6b3f36b2c --- /dev/null +++ b/src/django_rbac/migrations/0010_delete_operation.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.26 on 2022-10-11 14:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_rbac', '0009_auto_20221004_1343'), + ('a2_rbac', '0033_remove_old_operation_fk'), + ] + + operations = [ + migrations.DeleteModel( + name='Operation', + ), + ] diff --git a/src/django_rbac/models.py b/src/django_rbac/models.py deleted file mode 100644 index 90e5b2256..000000000 --- a/src/django_rbac/models.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from . import managers - - -class Operation(models.Model): - 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 = managers.OperationManager() - - -Operation._meta.natural_key = ['slug'] diff --git a/src/django_rbac/utils.py b/src/django_rbac/utils.py index 451131fe5..478690360 100644 --- a/src/django_rbac/utils.py +++ b/src/django_rbac/utils.py @@ -77,7 +77,7 @@ def get_permission_model(): def get_operation(operation_tpl): - from . import models + from authentic2.a2_rbac import models operation, dummy = models.Operation.objects.get_or_create(slug=operation_tpl.slug) return operation diff --git a/tests/test_a2_rbac.py b/tests/test_a2_rbac.py index 24fe0e594..a48843034 100644 --- a/tests/test_a2_rbac.py +++ b/tests/test_a2_rbac.py @@ -19,14 +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 CHANGE_OP, MANAGE_MEMBERS_OP +from authentic2.a2_rbac.models import CHANGE_OP, MANAGE_MEMBERS_OP, 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 tests.utils import login, request_select2, scoped_db_fixture @@ -691,3 +690,53 @@ class TestRole: def test_direct(self, db, fixture): assert set(fixture.role.children(direct=True)) == {fixture.role, fixture.child} assert fixture.role.children(include_self=False, direct=True).get() == fixture.child + + +def test_a2_rbac_operation_migration(migration, settings): + migrate_from = [ + ('a2_rbac', '0030_organizationalunit_min_password_strength'), + ('django_rbac', '0009_auto_20221004_1343'), + ] + migrate_to = [('a2_rbac', '0033_remove_old_operation_fk')] + + old_apps = migration.before(migrate_from) + ContentType = old_apps.get_model('contenttypes', 'ContentType') + Operation = old_apps.get_model('django_rbac', 'Operation') + Permission = old_apps.get_model('a2_rbac', 'Permission') + + # check objects created by signal handlers + base_operation = Operation.objects.get(slug='view') + base_permission = Permission.objects.filter(operation=base_operation).first() + + # check other objects + new_operation = Operation.objects.create(slug='test') + Permission.objects.create( + operation=new_operation, + target_ct=ContentType.objects.get_for_model(ContentType), + target_id=ContentType.objects.get_for_model(User).pk, + ) + + new_apps = migration.apply(migrate_to) + ContentType = new_apps.get_model('contenttypes', 'ContentType') + Operation = new_apps.get_model('a2_rbac', 'Operation') + Permission = new_apps.get_model('a2_rbac', 'Permission') + + base_operation = Operation.objects.get(slug='view') + assert ( + Permission.objects.filter( + operation_id=base_operation, + target_ct_id=base_permission.target_ct.pk, + target_id=base_permission.target_id, + ).count() + == 1 + ) + + new_operation = Operation.objects.get(slug=new_operation.slug) + assert ( + Permission.objects.filter( + operation=new_operation, + target_ct=ContentType.objects.get_for_model(ContentType), + target_id=ContentType.objects.get_for_model(User).pk, + ).count() + == 1 + ) diff --git a/tests/test_commands.py b/tests/test_commands.py index fe7be9d78..fba3c68c9 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -31,6 +31,7 @@ from authentic2.a2_rbac.models import ( ADMIN_OP, MANAGE_MEMBERS_OP, VIEW_OP, + Operation, OrganizationalUnit, Permission, Role, @@ -40,7 +41,6 @@ from authentic2.apps.journal.models import Event 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 Operation from django_rbac.utils import get_operation from .utils import call_command, login diff --git a/tests/test_rbac.py b/tests/test_rbac.py index 61aa69ee3..eb0eb2e05 100644 --- a/tests/test_rbac.py +++ b/tests/test_rbac.py @@ -20,8 +20,9 @@ from django.db import connection from django.db.models import Q from django.test.utils import CaptureQueriesContext +from authentic2.a2_rbac import models from authentic2.custom_user import backends -from django_rbac import models, utils +from django_rbac import utils OU = OrganizationalUnit = utils.get_ou_model() Permission = utils.get_permission_model() -- 2.35.1