diff --git a/awx/api/generics.py b/awx/api/generics.py index 4b6881156b..0b85ab8078 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -849,7 +849,7 @@ class ResourceAccessList(ParentMixin, ListAPIView): if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED: ancestors = set(RoleEvaluation.objects.filter(content_type_id=content_type.id, object_id=obj.id).values_list('role_id', flat=True)) qs = User.objects.filter(has_roles__in=ancestors) | User.objects.filter(is_superuser=True) - auditor_role = RoleDefinition.objects.filter(name="Controller System Auditor").first() + auditor_role = RoleDefinition.objects.filter(name="Platform Auditor").first() if auditor_role: qs |= User.objects.filter(role_assignments__role_definition=auditor_role) return qs.distinct() diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3e35a465a3..90c6d920b1 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3083,7 +3083,7 @@ class ResourceAccessListElementSerializer(UserSerializer): { "role": { "id": None, - "name": _("Controller System Auditor"), + "name": _("Platform Auditor"), "description": _("Can view all aspects of the system"), "user_capabilities": {"unattach": False}, }, diff --git a/awx/main/migrations/0202_convert_controller_role_definitions.py b/awx/main/migrations/0202_convert_controller_role_definitions.py new file mode 100644 index 0000000000..9a0c0b40fb --- /dev/null +++ b/awx/main/migrations/0202_convert_controller_role_definitions.py @@ -0,0 +1,102 @@ +# Generated by Django migration for converting Controller role definitions + +from ansible_base.rbac.migrations._utils import give_permissions +from django.db import migrations + + +def convert_controller_role_definitions(apps, schema_editor): + """ + Convert Controller role definitions to regular role definitions: + - Controller Organization Admin -> Organization Admin + - Controller Organization Member -> Organization Member + - Controller Team Admin -> Team Admin + - Controller Team Member -> Team Member + - Controller System Auditor -> Platform Auditor + + Then delete the old Controller role definitions. + """ + RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition') + RoleUserAssignment = apps.get_model('dab_rbac', 'RoleUserAssignment') + RoleTeamAssignment = apps.get_model('dab_rbac', 'RoleTeamAssignment') + Permission = apps.get_model('dab_rbac', 'DABPermission') + + # Mapping of old Controller role names to new role names + role_mappings = { + 'Controller Organization Admin': 'Organization Admin', + 'Controller Organization Member': 'Organization Member', + 'Controller Team Admin': 'Team Admin', + 'Controller Team Member': 'Team Member', + } + + for old_name, new_name in role_mappings.items(): + # Find the old Controller role definition + old_role = RoleDefinition.objects.filter(name=old_name).first() + if not old_role: + continue # Skip if the old role doesn't exist + + # Find the new role definition + new_role = RoleDefinition.objects.get(name=new_name) + + # Collect all the assignments that need to be migrated + # Group by object (content_type + object_id) to batch the give_permissions calls + assignments_by_object = {} + + # Get user assignments + user_assignments = RoleUserAssignment.objects.filter(role_definition=old_role).select_related('object_role') + for assignment in user_assignments: + key = (assignment.object_role.content_type_id, assignment.object_role.object_id) + if key not in assignments_by_object: + assignments_by_object[key] = {'users': [], 'teams': []} + assignments_by_object[key]['users'].append(assignment.user) + + # Get team assignments + team_assignments = RoleTeamAssignment.objects.filter(role_definition=old_role).select_related('object_role') + for assignment in team_assignments: + key = (assignment.object_role.content_type_id, assignment.object_role.object_id) + if key not in assignments_by_object: + assignments_by_object[key] = {'users': [], 'teams': []} + assignments_by_object[key]['teams'].append(assignment.team.id) + + # Use give_permissions to create new assignments with the new role definition + for (content_type_id, object_id), data in assignments_by_object.items(): + if data['users'] or data['teams']: + give_permissions( + apps, + new_role, + users=data['users'], + teams=data['teams'], + object_id=object_id, + content_type_id=content_type_id, + ) + + # Delete the old role definition (this will cascade to delete old assignments and ObjectRoles) + old_role.delete() + + # Create or get Platform Auditor + auditor_rd, created = RoleDefinition.objects.get_or_create( + name='Platform Auditor', + defaults={'description': 'Migrated singleton role giving read permission to everything', 'managed': True}, + ) + if created: + auditor_rd.permissions.add(*list(Permission.objects.filter(codename__startswith='view'))) + + old_rd = RoleDefinition.objects.filter(name='Controller System Auditor').first() + if old_rd: + for assignment in RoleUserAssignment.objects.filter(role_definition=old_rd): + RoleUserAssignment.objects.create( + user=assignment.user, + role_definition=auditor_rd, + ) + + # Delete the Controller System Auditor role + RoleDefinition.objects.filter(name='Controller System Auditor').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('main', '0201_create_managed_creds'), + ] + + operations = [ + migrations.RunPython(convert_controller_role_definitions), + ] diff --git a/awx/main/migrations/_dab_rbac.py b/awx/main/migrations/_dab_rbac.py index bb63d31a97..20a7e9633a 100644 --- a/awx/main/migrations/_dab_rbac.py +++ b/awx/main/migrations/_dab_rbac.py @@ -167,7 +167,7 @@ def migrate_to_new_rbac(apps, schema_editor): perm.delete() managed_definitions = dict() - for role_definition in RoleDefinition.objects.filter(managed=True).exclude(name__in=(settings.ANSIBLE_BASE_JWT_MANAGED_ROLES)): + for role_definition in RoleDefinition.objects.filter(managed=True): permissions = frozenset(role_definition.permissions.values_list('id', flat=True)) managed_definitions[permissions] = role_definition @@ -239,11 +239,14 @@ def migrate_to_new_rbac(apps, schema_editor): # Create new replacement system auditor role new_system_auditor, created = RoleDefinition.objects.get_or_create( - name='Controller System Auditor', + name='Platform Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything', 'managed': True}, ) new_system_auditor.permissions.add(*list(Permission.objects.filter(codename__startswith='view'))) + if created: + logger.info(f'Created RoleDefinition {new_system_auditor.name} pk={new_system_auditor.pk} with {new_system_auditor.permissions.count()} permissions') + # migrate is_system_auditor flag, because it is no longer handled by a system role old_system_auditor = Role.objects.filter(singleton_name='system_auditor').first() if old_system_auditor: @@ -272,7 +275,7 @@ def get_or_create_managed(name, description, ct, permissions, RoleDefinition): def setup_managed_role_definitions(apps, schema_editor): """ - Idepotent method to create or sync the managed role definitions + Idempotent method to create or sync the managed role definitions """ to_create = { 'object_admin': '{cls.__name__} Admin', @@ -309,16 +312,6 @@ def setup_managed_role_definitions(apps, schema_editor): to_create['object_admin'].format(cls=cls), f'Has all permissions to a single {cls._meta.verbose_name}', ct, indiv_perms, RoleDefinition ) ) - if cls_name == 'team': - managed_role_definitions.append( - get_or_create_managed( - 'Controller Team Admin', - f'Has all permissions to a single {cls._meta.verbose_name}', - ct, - indiv_perms, - RoleDefinition, - ) - ) if 'org_children' in to_create and (cls_name not in ('organization', 'instancegroup', 'team')): org_child_perms = object_perms.copy() @@ -359,18 +352,6 @@ def setup_managed_role_definitions(apps, schema_editor): RoleDefinition, ) ) - if action == 'member' and cls_name in ('organization', 'team'): - suffix = to_create['special'].format(cls=cls, action=action.title()) - rd_name = f'Controller {suffix}' - managed_role_definitions.append( - get_or_create_managed( - rd_name, - f'Has {action} permissions to a single {cls._meta.verbose_name}', - ct, - perm_list, - RoleDefinition, - ) - ) if 'org_admin' in to_create: managed_role_definitions.append( @@ -382,15 +363,6 @@ def setup_managed_role_definitions(apps, schema_editor): RoleDefinition, ) ) - managed_role_definitions.append( - get_or_create_managed( - 'Controller Organization Admin', - 'Has all permissions to a single organization and all objects inside of it', - org_ct, - org_perms, - RoleDefinition, - ) - ) # Special "organization action" roles audit_permissions = [perm for perm in org_perms if perm.codename.startswith('view_')] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 54eaab7c59..f45b8558ee 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -203,7 +203,7 @@ User.add_to_class('created', created) def get_system_auditor_role(): rd, created = RoleDefinition.objects.get_or_create( - name='Controller System Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything'} + name='Platform Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything'} ) if created: rd.permissions.add(*list(permission_registry.permission_qs.filter(codename__startswith='view'))) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 29edccab2c..775926a0c7 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -559,24 +559,12 @@ def get_role_definition(role): f = obj._meta.get_field(role.role_field) action_name = f.name.rsplit("_", 1)[0] model_print = type(obj).__name__ + rd_name = f'{model_print} {action_name.title()} Compat' perm_list = get_role_codenames(role) defaults = { 'content_type_id': role.content_type_id, 'description': f'Has {action_name.title()} permission to {model_print} for backwards API compatibility', } - # use Controller-specific role definitions for Team/Organization and member/admin - # instead of platform role definitions - # these should exist in the system already, so just do a lookup by role definition name - if model_print in ['Team', 'Organization'] and action_name in ['member', 'admin']: - rd_name = f'Controller {model_print} {action_name.title()}' - rd = RoleDefinition.objects.filter(name=rd_name).first() - if rd: - return rd - else: - return RoleDefinition.objects.create_from_permissions(permissions=perm_list, name=rd_name, managed=True, **defaults) - - else: - rd_name = f'{model_print} {action_name.title()} Compat' with impersonate(None): try: @@ -600,12 +588,6 @@ def get_role_from_object_role(object_role): model_name, role_name, _ = rd.name.split() role_name = role_name.lower() role_name += '_role' - elif rd.name.startswith('Controller') and rd.name.endswith(' Admin'): - # Controller Organization Admin and Controller Team Admin - role_name = 'admin_role' - elif rd.name.startswith('Controller') and rd.name.endswith(' Member'): - # Controller Organization Member and Controller Team Member - role_name = 'member_role' elif rd.name.endswith(' Admin') and rd.name.count(' ') == 2: # cases like "Organization Project Admin" model_name, target_model_name, role_name = rd.name.split() @@ -737,7 +719,6 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs) 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', @@ -762,11 +743,8 @@ ROLE_DEFINITION_TO_ROLE_FIELD = { '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', 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 314a55ae95..84e13c9895 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 @@ -146,14 +146,6 @@ def test_assign_credential_to_user_of_another_org(setup_managed_roles, credentia post(url=url, data={"user": org_admin.id, "role_definition": rd.id, "object_id": credential.id}, user=admin_user, expect=201) -@pytest.mark.django_db -def test_team_member_role_not_assignable(team, rando, post, admin_user, setup_managed_roles): - member_rd = RoleDefinition.objects.get(name='Organization Member') - url = django_reverse('roleuserassignment-list') - r = post(url, data={'object_id': team.id, 'role_definition': member_rd.id, 'user': rando.id}, user=admin_user, expect=400) - assert 'Not managed locally' in str(r.data) - - @pytest.mark.django_db def test_adding_user_to_org_member_role(setup_managed_roles, organization, admin, bob, post, get): ''' @@ -173,10 +165,16 @@ def test_adding_user_to_org_member_role(setup_managed_roles, organization, admin @pytest.mark.django_db @pytest.mark.parametrize('actor', ['user', 'team']) @pytest.mark.parametrize('role_name', ['Organization Admin', 'Organization Member', 'Team Admin', 'Team Member']) -def test_prevent_adding_actor_to_platform_roles(setup_managed_roles, role_name, actor, organization, team, admin, bob, post): +def test_adding_actor_to_platform_roles(setup_managed_roles, role_name, actor, organization, team, admin, bob, post): ''' - Prevent user or team from being added to platform-level roles + Allow user or team to be added to platform-level roles + Exceptions: + - Team cannot be added to Organization Member or Admin role ''' + if actor == 'team' and 'Organization' in role_name: + expect = 400 + else: + expect = 201 rd = RoleDefinition.objects.get(name=role_name) endpoint = 'roleuserassignment-list' if actor == 'user' else 'roleteamassignment-list' url = django_reverse(endpoint) @@ -184,37 +182,6 @@ def test_prevent_adding_actor_to_platform_roles(setup_managed_roles, role_name, data = {'object_id': object_id, 'role_definition': rd.id} actor_id = bob.id if actor == 'user' else team.id data[actor] = actor_id - r = post(url, data=data, user=admin, expect=400) - assert 'Not managed locally' in str(r.data) - - -@pytest.mark.django_db -@pytest.mark.parametrize('role_name', ['Controller Team Admin', 'Controller Team Member']) -def test_adding_user_to_controller_team_roles(setup_managed_roles, role_name, team, admin, bob, post, get): - ''' - Allow user to be added to Controller Team Admin or Controller Team Member - ''' - url_detail = reverse('api:team_detail', kwargs={'pk': team.id}) - get(url_detail, user=bob, expect=403) - - rd = RoleDefinition.objects.get(name=role_name) - url = django_reverse('roleuserassignment-list') - post(url, data={'object_id': team.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201) - - get(url_detail, user=bob, expect=200) - - -@pytest.mark.django_db -@pytest.mark.parametrize('role_name', ['Controller Organization Admin', 'Controller Organization Member']) -def test_adding_user_to_controller_organization_roles(setup_managed_roles, role_name, organization, admin, bob, post, get): - ''' - Allow user to be added to Controller Organization Admin or Controller Organization Member - ''' - url_detail = reverse('api:organization_detail', kwargs={'pk': organization.id}) - get(url_detail, user=bob, expect=403) - - rd = RoleDefinition.objects.get(name=role_name) - url = django_reverse('roleuserassignment-list') - post(url, data={'object_id': organization.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201) - - get(url, user=bob, expect=200) + r = post(url, data=data, user=admin, expect=expect) + if expect == 400: + assert 'Assigning organization member permission to teams is not allowed' in str(r.data) diff --git a/awx/main/tests/functional/dab_rbac/test_managed_roles.py b/awx/main/tests/functional/dab_rbac/test_managed_roles.py index 594428fdef..22fc16935c 100644 --- a/awx/main/tests/functional/dab_rbac/test_managed_roles.py +++ b/awx/main/tests/functional/dab_rbac/test_managed_roles.py @@ -31,32 +31,18 @@ def test_org_child_add_permission(setup_managed_roles): assert not DABPermission.objects.filter(codename='add_jobtemplate').exists() -@pytest.mark.django_db -def test_controller_specific_roles_have_correct_permissions(setup_managed_roles): - ''' - Controller specific roles should have the same permissions as the platform roles - e.g. Controller Team Admin should have same permission set as Team Admin - ''' - for rd_name in ['Controller Team Admin', 'Controller Team Member', 'Controller Organization Member', 'Controller Organization Admin']: - rd = RoleDefinition.objects.get(name=rd_name) - rd_platform = RoleDefinition.objects.get(name=rd_name.split('Controller ')[1]) - assert set(rd.permissions.all()) == set(rd_platform.permissions.all()) - - @pytest.mark.django_db @pytest.mark.parametrize('resource_name', ['Team', 'Organization']) @pytest.mark.parametrize('action', ['Member', 'Admin']) -def test_legacy_RBAC_uses_controller_specific_roles(setup_managed_roles, resource_name, action, team, bob, organization): +def test_legacy_RBAC_uses_platform_roles(setup_managed_roles, resource_name, action, team, bob, organization): ''' - Assignment to legacy RBAC roles should use controller specific role definitions - e.g. Controller Team Admin, Controller Team Member, Controller Organization Member, Controller Organization Admin + Assignment to legacy RBAC roles should use platform role definitions + e.g. Team Admin, Team Member, Organization Member, Organization Admin ''' resource = team if resource_name == 'Team' else organization if action == 'Member': resource.member_role.members.add(bob) else: resource.admin_role.members.add(bob) - rd = RoleDefinition.objects.get(name=f'Controller {resource_name} {action}') - rd_platform = RoleDefinition.objects.get(name=f'{resource_name} {action}') + rd = RoleDefinition.objects.get(name=f'{resource_name} {action}') assert RoleUserAssignment.objects.filter(role_definition=rd, user=bob, object_id=resource.id).exists() - assert not RoleUserAssignment.objects.filter(role_definition=rd_platform, user=bob, object_id=resource.id).exists() diff --git a/awx/main/tests/functional/dab_rbac/test_translation_layer.py b/awx/main/tests/functional/dab_rbac/test_translation_layer.py index 5d1e519a39..5134b8ce3e 100644 --- a/awx/main/tests/functional/dab_rbac/test_translation_layer.py +++ b/awx/main/tests/functional/dab_rbac/test_translation_layer.py @@ -208,19 +208,19 @@ def test_user_auditor_rel(organization, rando, setup_managed_roles): @pytest.mark.django_db @pytest.mark.parametrize('resource_name', ['Organization', 'Team']) @pytest.mark.parametrize('role_name', ['Member', 'Admin']) -def test_mapping_from_controller_role_definitions_to_roles(organization, team, rando, role_name, resource_name, setup_managed_roles): +def test_mapping_from_role_definitions_to_roles(organization, team, rando, role_name, resource_name, setup_managed_roles): """ - ensure mappings for controller roles are correct + ensure mappings for platform roles are correct e.g. - Controller Organization Member > organization.member_role - Controller Organization Admin > organization.admin_role - Controller Team Member > team.member_role - Controller Team Admin > team.admin_role + Organization Member > organization.member_role + Organization Admin > organization.admin_role + Team Member > team.member_role + Team Admin > team.admin_role """ resource = organization if resource_name == 'Organization' else team old_role_name = f"{role_name.lower()}_role" getattr(resource, old_role_name).members.add(rando) assignment = RoleUserAssignment.objects.get(user=rando) - assert assignment.role_definition.name == f'Controller {resource_name} {role_name}' + assert assignment.role_definition.name == f'{resource_name} {role_name}' old_role = get_role_from_object_role(assignment.object_role) assert old_role.id == getattr(resource, old_role_name).id 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 index 946c76179f..92efef8387 100644 --- 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 @@ -35,21 +35,21 @@ class TestNewToOld: 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 + Assign user to Team Member role definition, should be added to team.member_role.members ''' - rd = RoleDefinition.objects.get(name='Controller Team Member') + rd = RoleDefinition.objects.get(name='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): + def test_new_to_old_rbac_team_member_removal(self, admin, delete, team, bob, setup_managed_roles): ''' - Remove user from Controller Team Member role definition, should be deleted from team.member_role.members + Remove user from 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') + rd = RoleDefinition.objects.get(name='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}) diff --git a/awx/main/tests/functional/test_migrations.py b/awx/main/tests/functional/test_migrations.py index 24f872f088..83fca2cbeb 100644 --- a/awx/main/tests/functional/test_migrations.py +++ b/awx/main/tests/functional/test_migrations.py @@ -90,8 +90,8 @@ class TestMigrationSmoke: RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment') assert RoleUserAssignment.objects.filter(user=user.id, object_id=org.id).exists() - assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Organization Member', object_id=org.id).exists() - assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Team Member', object_id=team.id).exists() + assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Organization Member', object_id=org.id).exists() + assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Team Member', object_id=team.id).exists() # Regression testing for bug that comes from current vs past models mismatch RoleDefinition = new_state.apps.get_model('dab_rbac', 'RoleDefinition') diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9886349a49..89f12f07dd 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -683,7 +683,7 @@ AWX_AUTO_DEPROVISION_INSTANCES = False # If True, allow users to be assigned to roles that were created via JWT -ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False +ALLOW_LOCAL_ASSIGNING_JWT_ROLES = True # Enable Pendo on the UI, possible values are 'off', 'anonymous', and 'detailed' # Note: This setting may be overridden by database settings.