diff --git a/awx/main/migrations/_dab_rbac.py b/awx/main/migrations/_dab_rbac.py index f97c80e29ab9..5082a72e4fca 100644 --- a/awx/main/migrations/_dab_rbac.py +++ b/awx/main/migrations/_dab_rbac.py @@ -275,7 +275,12 @@ def setup_managed_role_definitions(apps, schema_editor): """ Idepotent method to create or sync the managed role definitions """ - to_create = settings.ANSIBLE_BASE_ROLE_PRECREATE + to_create = { + 'object_admin': '{cls.__name__} Admin', + 'org_admin': 'Organization Admin', + 'org_children': 'Organization {cls.__name__} Admin', + 'special': '{cls.__name__} {action}', + } ContentType = apps.get_model('contenttypes', 'ContentType') Permission = apps.get_model('dab_rbac', 'DABPermission') diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index c3cdeb5f6b2a..3dfb583b42da 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -10,6 +10,9 @@ # django-rest-framework from rest_framework.serializers import ValidationError +# crum to impersonate users +from crum import impersonate + # Django from django.db import models, transaction, connection from django.db.models.signals import m2m_changed @@ -553,17 +556,22 @@ def get_role_definition(role): return f = obj._meta.get_field(role.role_field) action_name = f.name.rsplit("_", 1)[0] - rd_name = f'{type(obj).__name__} {action_name.title()} Compat' + 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} - try: - rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults) - except ValidationError: - # This is a tricky case - practically speaking, users should not be allowed to create team roles - # or roles that include the team member permission. - # If we need to create this for compatibility purposes then we will create it as a managed non-editable role - defaults['managed'] = True - rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults) + defaults = { + 'content_type_id': role.content_type_id, + 'description': f'Has {action_name.title()} permission to {model_print} for backwards API compatibility', + } + with impersonate(None): + try: + rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults) + except ValidationError: + # This is a tricky case - practically speaking, users should not be allowed to create team roles + # or roles that include the team member permission. + # If we need to create this for compatibility purposes then we will create it as a managed non-editable role + defaults['managed'] = True + rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults) return rd diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index f95889470258..65e807424175 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -30,7 +30,7 @@ def test_idempotent_credential_type_setup(): @pytest.mark.django_db -def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh): +def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, setup_managed_roles): params = { 'credential_type': 1, 'inputs': {'username': 'someusername'}, @@ -81,7 +81,7 @@ def test_credential_validation_error_with_multiple_owner_fields(post, admin, ali @pytest.mark.django_db -def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh): +def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, setup_managed_roles): params = { 'credential_type': 1, 'inputs': {'username': 'someusername'}, diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 8c68bd91eefa..abecda397e2c 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -16,6 +16,8 @@ from django.db.models.signals import post_migrate +from awx.main.migrations._dab_rbac import setup_managed_role_definitions + # AWX from awx.main.models.projects import Project from awx.main.models.ha import Instance @@ -90,6 +92,12 @@ def deploy_jobtemplate(project, inventory, credential): return jt +@pytest.fixture +def setup_managed_roles(): + "Run the migration script to pre-create managed role definitions" + setup_managed_role_definitions(apps, None) + + @pytest.fixture def team(organization): return organization.teams.create(name='test-team') diff --git a/awx/main/tests/functional/dab_rbac/conftest.py b/awx/main/tests/functional/dab_rbac/conftest.py deleted file mode 100644 index 2e37b7f7514c..000000000000 --- a/awx/main/tests/functional/dab_rbac/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest -from django.apps import apps - -from awx.main.migrations._dab_rbac import setup_managed_role_definitions - - -@pytest.fixture -def managed_roles(): - "Run the migration script to pre-create managed role definitions" - setup_managed_role_definitions(apps, None) diff --git a/awx/main/tests/functional/dab_rbac/test_dab_migration.py b/awx/main/tests/functional/dab_rbac/test_dab_migration.py deleted file mode 100644 index 34639774db68..000000000000 --- a/awx/main/tests/functional/dab_rbac/test_dab_migration.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from django.apps import apps -from django.test.utils import override_settings - -from awx.main.migrations._dab_rbac import setup_managed_role_definitions - -from ansible_base.rbac.models import RoleDefinition - -INVENTORY_OBJ_PERMISSIONS = ['view_inventory', 'adhoc_inventory', 'use_inventory', 'change_inventory', 'delete_inventory', 'update_inventory'] - - -@pytest.mark.django_db -def test_managed_definitions_precreate(): - with override_settings( - ANSIBLE_BASE_ROLE_PRECREATE={ - 'object_admin': '{cls._meta.model_name}-admin', - 'org_admin': 'organization-admin', - 'org_children': 'organization-{cls._meta.model_name}-admin', - 'special': '{cls._meta.model_name}-{action}', - } - ): - setup_managed_role_definitions(apps, None) - rd = RoleDefinition.objects.get(name='inventory-admin') - assert rd.managed is True - # add permissions do not go in the object-level admin - assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS) - - # test org-level object admin permissions - rd = RoleDefinition.objects.get(name='organization-inventory-admin') - assert rd.managed is True - assert set(rd.permissions.values_list('codename', flat=True)) == set(['add_inventory', 'view_organization'] + INVENTORY_OBJ_PERMISSIONS) - - -@pytest.mark.django_db -def test_managed_definitions_custom_obj_admin_name(): - with override_settings( - ANSIBLE_BASE_ROLE_PRECREATE={ - 'object_admin': 'foo-{cls._meta.model_name}-foo', - } - ): - setup_managed_role_definitions(apps, None) - rd = RoleDefinition.objects.get(name='foo-inventory-foo') - assert rd.managed is True - # add permissions do not go in the object-level admin - assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS) 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 20041eebd3e6..d9e3453ef254 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 @@ -10,7 +10,7 @@ @pytest.mark.django_db -def test_managed_roles_created(managed_roles): +def test_managed_roles_created(setup_managed_roles): "Managed RoleDefinitions are created in post_migration signal, we expect to see them here" for cls in (JobTemplate, Inventory): ct = ContentType.objects.get_for_model(cls) @@ -22,7 +22,7 @@ def test_managed_roles_created(managed_roles): @pytest.mark.django_db -def test_custom_read_role(admin_user, post, managed_roles): +def test_custom_read_role(admin_user, post, setup_managed_roles): rd_url = django_reverse('roledefinition-list') resp = post( url=rd_url, data={"name": "read role made for test", "content_type": "awx.inventory", "permissions": ['view_inventory']}, user=admin_user, expect=201 @@ -40,7 +40,7 @@ def test_custom_system_roles_prohibited(admin_user, post): @pytest.mark.django_db -def test_assignment_to_invisible_user(admin_user, alice, rando, inventory, post, managed_roles): +def test_assignment_to_invisible_user(admin_user, alice, rando, inventory, post, setup_managed_roles): "Alice can not see rando, and so can not give them a role assignment" rd = RoleDefinition.objects.get(name='Inventory Admin') rd.give_permission(alice, inventory) @@ -51,7 +51,7 @@ def test_assignment_to_invisible_user(admin_user, alice, rando, inventory, post, @pytest.mark.django_db -def test_assign_managed_role(admin_user, alice, rando, inventory, post, managed_roles, organization): +def test_assign_managed_role(admin_user, alice, rando, inventory, post, setup_managed_roles, organization): rd = RoleDefinition.objects.get(name='Inventory Admin') rd.give_permission(alice, inventory) # When alice and rando are members of the same org, they can see each other @@ -78,7 +78,7 @@ def test_assign_custom_delete_role(admin_user, rando, inventory, delete, patch): @pytest.mark.django_db -def test_assign_custom_add_role(admin_user, rando, organization, post, managed_roles): +def test_assign_custom_add_role(admin_user, rando, organization, post, setup_managed_roles): rd, _ = RoleDefinition.objects.get_or_create( name='inventory-add', permissions=['add_inventory', 'view_organization'], content_type=ContentType.objects.get_for_model(Organization) ) 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 2829599252e4..c8e529c902cd 100644 --- a/awx/main/tests/functional/dab_rbac/test_translation_layer.py +++ b/awx/main/tests/functional/dab_rbac/test_translation_layer.py @@ -2,11 +2,15 @@ import pytest +from django.contrib.contenttypes.models import ContentType + +from crum import impersonate + from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions from awx.main.models import User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team from awx.api.versioning import reverse -from ansible_base.rbac.models import RoleUserAssignment +from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition @pytest.mark.django_db @@ -14,7 +18,7 @@ 'role_name', ['execution_environment_admin_role', 'project_admin_role', 'admin_role', 'auditor_role', 'read_role', 'execute_role', 'notification_admin_role'], ) -def test_round_trip_roles(organization, rando, role_name, managed_roles): +def test_round_trip_roles(organization, rando, role_name, setup_managed_roles): """ Make an assignment with the old-style role, get the equivelent new role @@ -28,7 +32,39 @@ def test_round_trip_roles(organization, rando, role_name, managed_roles): @pytest.mark.django_db -def test_organization_level_permissions(organization, inventory, managed_roles): +def test_role_naming(setup_managed_roles): + qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='dmin') + assert qs.count() == 1 # sanity + rd = qs.first() + assert rd.name == 'JobTemplate Admin' + assert rd.description + assert rd.created_by is None + + +@pytest.mark.django_db +def test_action_role_naming(setup_managed_roles): + qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='ecute') + assert qs.count() == 1 # sanity + rd = qs.first() + assert rd.name == 'JobTemplate Execute' + assert rd.description + assert rd.created_by is None + + +@pytest.mark.django_db +def test_compat_role_naming(setup_managed_roles, job_template, rando, alice): + with impersonate(alice): + job_template.read_role.members.add(rando) + qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='ompat') + assert qs.count() == 1 # sanity + rd = qs.first() + assert rd.name == 'JobTemplate Read Compat' + assert rd.description + assert rd.created_by is None + + +@pytest.mark.django_db +def test_organization_level_permissions(organization, inventory, setup_managed_roles): u1 = User.objects.create(username='alice') u2 = User.objects.create(username='bob') @@ -58,14 +94,14 @@ def test_organization_level_permissions(organization, inventory, managed_roles): @pytest.mark.django_db -def test_organization_execute_role(organization, rando, managed_roles): +def test_organization_execute_role(organization, rando, setup_managed_roles): organization.execute_role.members.add(rando) assert rando in organization.execute_role assert set(Organization.accessible_objects(rando, 'execute_role')) == set([organization]) @pytest.mark.django_db -def test_workflow_approval_list(get, post, admin_user, managed_roles): +def test_workflow_approval_list(get, post, admin_user, setup_managed_roles): workflow_job_template = WorkflowJobTemplate.objects.create() approval_node = WorkflowJobTemplateNode.objects.create(workflow_job_template=workflow_job_template) url = reverse('api:workflow_job_template_node_create_approval', kwargs={'pk': approval_node.pk, 'version': 'v2'}) @@ -79,14 +115,14 @@ def test_workflow_approval_list(get, post, admin_user, managed_roles): @pytest.mark.django_db -def test_creator_permission(rando, admin_user, inventory, managed_roles): +def test_creator_permission(rando, admin_user, inventory, setup_managed_roles): give_creator_permissions(rando, inventory) assert rando in inventory.admin_role assert rando in inventory.admin_role.members.all() @pytest.mark.django_db -def test_team_team_read_role(rando, team, admin_user, post, managed_roles): +def test_team_team_read_role(rando, team, admin_user, post, setup_managed_roles): orgs = [Organization.objects.create(name=f'foo-{i}') for i in range(2)] teams = [Team.objects.create(name=f'foo-{i}', organization=orgs[i]) for i in range(2)] teams[1].member_role.members.add(rando) diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 17e7ff3524b0..0b73dc34cb76 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -165,7 +165,7 @@ def test_system_admin_orphan_capabilities(self, job_template, admin_user): @pytest.mark.django_db @pytest.mark.job_permissions -def test_job_template_creator_access(project, organization, rando, post): +def test_job_template_creator_access(project, organization, rando, post, setup_managed_roles): project.use_role.members.add(rando) response = post( url=reverse('api:job_template_list'), diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index c9270863543c..5176dc2b8454 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -1148,13 +1148,8 @@ # Settings for the ansible_base RBAC system -# Only used internally, names of the managed RoleDefinitions to create -ANSIBLE_BASE_ROLE_PRECREATE = { - 'object_admin': '{cls.__name__} Admin', - 'org_admin': 'Organization Admin', - 'org_children': 'Organization {cls.__name__} Admin', - 'special': '{cls.__name__} {action}', -} +# This has been moved to data migration code +ANSIBLE_BASE_ROLE_PRECREATE = {} # Name for auto-created roles that give users permissions to what they create ANSIBLE_BASE_ROLE_CREATOR_NAME = '{cls.__name__} Creator'