From 7374a6aad9e378924df6152a240a56f91765a8b2 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 4 Sep 2024 14:36:24 -0400 Subject: [PATCH 1/5] Translate new RBAC to old RBAC User and Team assignments using the DAB RBAC system will be translated back to the old Role system. This ensures better backward compatibility and addresses some inconsistences in the UI that were relying on older RBAC endpoints. Signed-off-by: Seth Foster --- awx/api/generics.py | 3 + awx/main/models/rbac.py | 84 ++++++++++++++++++- .../functional/dab_rbac/test_dab_rbac_api.py | 20 ++++- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index a059ca834ffb..3dc31df7d1cf 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -667,6 +667,9 @@ def attach_validate(self, request): return (sub_id, res) def attach(self, request, *args, **kwargs): + import sdb + + sdb.set_trace() created = False parent = self.get_parent_object() relationship = getattrd(parent, self.relationship) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 0bff43a3b09e..958272141ec0 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -15,16 +15,18 @@ # Django from django.db import models, transaction, connection -from django.db.models.signals import m2m_changed +from django.db.models.signals import m2m_changed, post_save, post_delete +from django.dispatch import receiver from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist from django.apps import apps from django.conf import settings # Ansible_base app -from ansible_base.rbac.models import RoleDefinition +from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment, RoleTeamAssignment from ansible_base.lib.utils.models import get_type_for_model # AWX @@ -733,5 +735,83 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs) give_or_remove_permission(child_role, team, giving=is_giving) +ROLE_DEFINITION_TO_ROLE_FIELD = { + 'Organization Member': 'member_role', + 'Controller Organization Member': 'member_role', + 'WorkflowJobTemplate Admin': 'admin_role', + 'Organization WorkflowJobTemplate Admin': 'workflow_admin_role', + 'WorkflowJobTemplate Execute': 'execute_role', + 'WorkflowJobTemplate Approve': 'approval_role', + 'InstanceGroup Admin': 'admin_role', + 'InstanceGroup Use': 'use_role', + # 'ExecutionEnvironment Admin': '', + 'Organization ExecutionEnvironment Admin': 'execution_environment_admin_role', + 'Project Admin': 'admin_role', + 'Organization Project Admin': 'project_admin_role', + 'Project Use': 'use_role', + 'Project Update': 'update_role', + 'JobTemplate Admin': 'admin_role', + 'Organization JobTemplate Admin': 'job_template_admin_role', + 'JobTemplate Execute': 'execute_role', + 'Inventory Admin': 'admin_role', + 'Organization Inventory Admin': 'inventory_admin_role', + 'Inventory Use': 'use_role', + 'Inventory Adhoc': 'adhoc_role', + 'Inventory Update': 'update_role', + # 'NotificationTemplate Admin': 'admin_role', + 'Organization NotificationTemplate Admin': 'notification_admin_role', + 'Credential Admin': 'admin_role', + 'Organization Credential Admin': 'credential_admin_role', + 'Credential Use': 'use_role', + 'Team Admin': 'admin_role', + 'Controller Team Admin': 'admin_role', + 'Team Member': 'member_role', + 'Controller Team Member': 'member_role', + 'Organization Admin': 'admin_role', + 'Controller Organization Admin': 'admin_role', + 'Organization Audit': 'auditor_role', + 'Organization Execute': 'execute_role', + 'Organization Approval': 'approval_role', +} + + +def _sync_assignments_to_old_rbac(instance, delete=True): + with disable_rbac_sync(): + field_name = ROLE_DEFINITION_TO_ROLE_FIELD.get(instance.role_definition.name) + if not field_name: + return + try: + role = getattr(instance.object_role.content_object, field_name) + # in the case RoleUserAssignment is being cascade deleted, then + # object_role might not exist. In which case the object is about to be removed + # anyways so just return + except ObjectDoesNotExist: + return + if isinstance(instance.actor, get_user_model()): + # user + if delete: + role.members.remove(instance.actor) + else: + role.members.add(instance.actor) + else: + # team + if delete: + instance.team.member_role.children.remove(role) + else: + instance.team.member_role.children.add(role) + + +@receiver(post_delete, sender=RoleUserAssignment) +@receiver(post_delete, sender=RoleTeamAssignment) +def sync_assignments_to_old_rbac_delete(instance, **kwargs): + _sync_assignments_to_old_rbac(instance, delete=True) + + +@receiver(post_save, sender=RoleUserAssignment) +@receiver(post_save, sender=RoleTeamAssignment) +def sync_user_assignments_to_old_rbac_create(instance, **kwargs): + _sync_assignments_to_old_rbac(instance, delete=False) + + m2m_changed.connect(sync_members_to_new_rbac, Role.members.through) m2m_changed.connect(sync_parents_to_new_rbac, Role.parents.through) diff --git a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py index 314a55ae952a..f5cbac1c5d74 100644 --- a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py +++ b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py @@ -7,7 +7,8 @@ from awx.main.models import JobTemplate, Inventory, Organization from awx.main.access import JobTemplateAccess, WorkflowJobTemplateAccess -from ansible_base.rbac.models import RoleDefinition +from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment +from ansible_base.lib.utils.response import get_relative_url @pytest.mark.django_db @@ -218,3 +219,20 @@ def test_adding_user_to_controller_organization_roles(setup_managed_roles, role_ post(url, data={'object_id': organization.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201) get(url, user=bob, expect=200) + + +@pytest.mark.django_db +def test_new_to_old_rbac(admin, delete, team, bob, post): + ''' + Assignments in new RBAC should be reflected in old RBAC + ''' + role = team.member_role + url = reverse('api:role_users_list', kwargs={'pk': role.id}) + post(url, data={'id': bob.id}, user=admin, expect=204) + + rd = RoleDefinition.objects.get(name='Controller Team Member') + user_assignment = RoleUserAssignment.objects.get(user=bob, role_definition=rd, object_id=team.id) + + url = get_relative_url('roleuserassignment-detail', kwargs={'pk': user_assignment.id}) + delete(url, user=admin, expect=204) + assert bob not in team.member_role.members.all() From f71dca7327f4e39257833697b82874e6c6cda798 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 4 Sep 2024 14:42:17 -0400 Subject: [PATCH 2/5] remove debug point Signed-off-by: Seth Foster --- awx/api/generics.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 3dc31df7d1cf..a059ca834ffb 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -667,9 +667,6 @@ def attach_validate(self, request): return (sub_id, res) def attach(self, request, *args, **kwargs): - import sdb - - sdb.set_trace() created = False parent = self.get_parent_object() relationship = getattrd(parent, self.relationship) From 54886809151f61488b776c3b41ff6d243df3f453 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 5 Sep 2024 13:30:29 -0400 Subject: [PATCH 3/5] extend test coverage for syncing new to old Signed-off-by: Seth Foster --- awx/main/models/rbac.py | 49 ++++++------ .../functional/dab_rbac/test_dab_rbac_api.py | 17 ---- .../test_translation_layer_new_to_old.py | 80 +++++++++++++++++++ 3 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 awx/main/tests/functional/dab_rbac/test_translation_layer_new_to_old.py diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 958272141ec0..29edccab2cd5 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -744,7 +744,6 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs) 'WorkflowJobTemplate Approve': 'approval_role', 'InstanceGroup Admin': 'admin_role', 'InstanceGroup Use': 'use_role', - # 'ExecutionEnvironment Admin': '', 'Organization ExecutionEnvironment Admin': 'execution_environment_admin_role', 'Project Admin': 'admin_role', 'Organization Project Admin': 'project_admin_role', @@ -758,7 +757,6 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs) 'Inventory Use': 'use_role', 'Inventory Adhoc': 'adhoc_role', 'Inventory Update': 'update_role', - # 'NotificationTemplate Admin': 'admin_role', 'Organization NotificationTemplate Admin': 'notification_admin_role', 'Credential Admin': 'admin_role', 'Organization Credential Admin': 'credential_admin_role', @@ -776,29 +774,32 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs) def _sync_assignments_to_old_rbac(instance, delete=True): - with disable_rbac_sync(): - field_name = ROLE_DEFINITION_TO_ROLE_FIELD.get(instance.role_definition.name) - if not field_name: - return - try: - role = getattr(instance.object_role.content_object, field_name) - # in the case RoleUserAssignment is being cascade deleted, then - # object_role might not exist. In which case the object is about to be removed - # anyways so just return - except ObjectDoesNotExist: - return - if isinstance(instance.actor, get_user_model()): - # user - if delete: - role.members.remove(instance.actor) - else: - role.members.add(instance.actor) - else: - # team - if delete: - instance.team.member_role.children.remove(role) + from awx.main.signals import disable_activity_stream + + with disable_activity_stream(): + with disable_rbac_sync(): + field_name = ROLE_DEFINITION_TO_ROLE_FIELD.get(instance.role_definition.name) + if not field_name: + return + try: + role = getattr(instance.object_role.content_object, field_name) + # in the case RoleUserAssignment is being cascade deleted, then + # object_role might not exist. In which case the object is about to be removed + # anyways so just return + except ObjectDoesNotExist: + return + if isinstance(instance.actor, get_user_model()): + # user + if delete: + role.members.remove(instance.actor) + else: + role.members.add(instance.actor) else: - instance.team.member_role.children.add(role) + # team + if delete: + instance.team.member_role.children.remove(role) + else: + instance.team.member_role.children.add(role) @receiver(post_delete, sender=RoleUserAssignment) diff --git a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py index f5cbac1c5d74..909b985c9782 100644 --- a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py +++ b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py @@ -219,20 +219,3 @@ def test_adding_user_to_controller_organization_roles(setup_managed_roles, role_ post(url, data={'object_id': organization.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201) get(url, user=bob, expect=200) - - -@pytest.mark.django_db -def test_new_to_old_rbac(admin, delete, team, bob, post): - ''' - Assignments in new RBAC should be reflected in old RBAC - ''' - role = team.member_role - url = reverse('api:role_users_list', kwargs={'pk': role.id}) - post(url, data={'id': bob.id}, user=admin, expect=204) - - rd = RoleDefinition.objects.get(name='Controller Team Member') - user_assignment = RoleUserAssignment.objects.get(user=bob, role_definition=rd, object_id=team.id) - - url = get_relative_url('roleuserassignment-detail', kwargs={'pk': user_assignment.id}) - delete(url, user=admin, expect=204) - assert bob not in team.member_role.members.all() diff --git a/awx/main/tests/functional/dab_rbac/test_translation_layer_new_to_old.py b/awx/main/tests/functional/dab_rbac/test_translation_layer_new_to_old.py new file mode 100644 index 000000000000..946c76179f39 --- /dev/null +++ b/awx/main/tests/functional/dab_rbac/test_translation_layer_new_to_old.py @@ -0,0 +1,80 @@ +from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment, RoleTeamAssignment +from ansible_base.lib.utils.response import get_relative_url +import pytest + + +@pytest.mark.django_db +class TestNewToOld: + ''' + Tests that the DAB RBAC system is correctly translated to the old RBAC system + Namely, tests functionality of the _sync_assignments_to_old_rbac signal handler + ''' + + def test_new_to_old_rbac_addition(self, admin, post, inventory, bob, setup_managed_roles): + ''' + Assign user to Inventory Admin role definition, should be added to inventory.admin_role.members + ''' + rd = RoleDefinition.objects.get(name='Inventory Admin') + + url = get_relative_url('roleuserassignment-list') + post(url, user=admin, data={'role_definition': rd.id, 'user': bob.id, 'object_id': inventory.id}, expect=201) + assert bob in inventory.admin_role.members.all() + + def test_new_to_old_rbac_removal(self, admin, delete, inventory, bob, setup_managed_roles): + ''' + Remove user from Inventory Admin role definition, should be deleted from inventory.admin_role.members + ''' + inventory.admin_role.members.add(bob) + + rd = RoleDefinition.objects.get(name='Inventory Admin') + user_assignment = RoleUserAssignment.objects.get(user=bob, role_definition=rd, object_id=inventory.id) + + url = get_relative_url('roleuserassignment-detail', kwargs={'pk': user_assignment.id}) + delete(url, user=admin, expect=204) + assert bob not in inventory.admin_role.members.all() + + def test_new_to_old_rbac_team_member_addition(self, admin, post, team, bob, setup_managed_roles): + ''' + Assign user to Controller Team Member role definition, should be added to team.member_role.members + ''' + rd = RoleDefinition.objects.get(name='Controller Team Member') + + url = get_relative_url('roleuserassignment-list') + post(url, user=admin, data={'role_definition': rd.id, 'user': bob.id, 'object_id': team.id}, expect=201) + assert bob in team.member_role.members.all() + + def test_new_to_old_rbac_team_member_removal(self, admin, delete, team, bob): + ''' + Remove user from Controller Team Member role definition, should be deleted from team.member_role.members + ''' + team.member_role.members.add(bob) + + rd = RoleDefinition.objects.get(name='Controller Team Member') + user_assignment = RoleUserAssignment.objects.get(user=bob, role_definition=rd, object_id=team.id) + + url = get_relative_url('roleuserassignment-detail', kwargs={'pk': user_assignment.id}) + delete(url, user=admin, expect=204) + assert bob not in team.member_role.members.all() + + def test_new_to_old_rbac_team_addition(self, admin, post, team, inventory, setup_managed_roles): + ''' + Assign team to Inventory Admin role definition, should be added to inventory.admin_role.parents + ''' + rd = RoleDefinition.objects.get(name='Inventory Admin') + + url = get_relative_url('roleteamassignment-list') + post(url, user=admin, data={'role_definition': rd.id, 'team': team.id, 'object_id': inventory.id}, expect=201) + assert team.member_role in inventory.admin_role.parents.all() + + def test_new_to_old_rbac_team_removal(self, admin, delete, team, inventory, setup_managed_roles): + ''' + Remove team from Inventory Admin role definition, should be deleted from inventory.admin_role.parents + ''' + inventory.admin_role.parents.add(team.member_role) + + rd = RoleDefinition.objects.get(name='Inventory Admin') + team_assignment = RoleTeamAssignment.objects.get(team=team, role_definition=rd, object_id=inventory.id) + + url = get_relative_url('roleteamassignment-detail', kwargs={'pk': team_assignment.id}) + delete(url, user=admin, expect=204) + assert team.member_role not in inventory.admin_role.parents.all() From 10f73560bfbe2bd412b7643c0a59b5de57e1063f Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 5 Sep 2024 13:31:54 -0400 Subject: [PATCH 4/5] remove unused imports Signed-off-by: Seth Foster --- awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py index 909b985c9782..97d224f062ed 100644 --- a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py +++ b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py @@ -7,9 +7,6 @@ from awx.main.models import JobTemplate, Inventory, Organization from awx.main.access import JobTemplateAccess, WorkflowJobTemplateAccess -from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment -from ansible_base.lib.utils.response import get_relative_url - @pytest.mark.django_db def test_managed_roles_created(setup_managed_roles): From 58d341a6d93f653fb94f93954904f9671ee039ea Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 5 Sep 2024 15:53:47 -0400 Subject: [PATCH 5/5] reimport Signed-off-by: Seth Foster --- awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py index 97d224f062ed..314a55ae952a 100644 --- a/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py +++ b/awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py @@ -7,6 +7,8 @@ from awx.main.models import JobTemplate, Inventory, Organization from awx.main.access import JobTemplateAccess, WorkflowJobTemplateAccess +from ansible_base.rbac.models import RoleDefinition + @pytest.mark.django_db def test_managed_roles_created(setup_managed_roles):