From e7e83afd00cef9c8655aa571923b4411c83f0e42 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 26 Jan 2018 00:33:07 +0000 Subject: [PATCH 01/10] Add Project Admin role --- awx/main/access.py | 4 ++-- awx/main/models/organization.py | 8 +++++++- awx/main/models/projects.py | 2 +- awx/main/models/rbac.py | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 5ce76a52f8..400af5e267 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1087,8 +1087,8 @@ class ProjectAccess(BaseAccess): @check_superuser def can_add(self, data): if not data: # So the browseable API will work - return Organization.accessible_objects(self.user, 'admin_role').exists() - return self.check_related('organization', Organization, data, mandatory=True) + return Organization.accessible_objects(self.user, 'project_admin_role').exists() + return self.check_related('organization', Organization, data, role_field='project_admin_role', mandatory=True) @check_superuser def can_change(self, obj, data): diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index cd0ccfc785..c82d911ecb 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -43,11 +43,17 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi admin_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ) + project_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) + inventory_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - parent_role='admin_role', + parent_role=['admin_role', 'project_admin_role', 'inventory_admin_role'] ) read_role = ImplicitRoleField( parent_role=['member_role', 'auditor_role'], diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index ef3f809a74..77d77bf13e 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -284,7 +284,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn ) admin_role = ImplicitRoleField(parent_role=[ - 'organization.admin_role', + 'organization.project_admin_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ]) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index b7f70aafec..62e9348baf 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -37,6 +37,7 @@ role_names = { 'system_auditor' : _('System Auditor'), 'adhoc_role' : _('Ad Hoc'), 'admin_role' : _('Admin'), + 'project_admin_role' : _('Project Admin'), 'auditor_role' : _('Auditor'), 'execute_role' : _('Execute'), 'member_role' : _('Member'), @@ -50,6 +51,7 @@ role_descriptions = { 'system_auditor' : _('Can view all settings on the system'), 'adhoc_role' : _('May run ad hoc commands on an inventory'), 'admin_role' : _('Can manage all aspects of the %s'), + 'project_admin_role' : _('Can manage all projects of the %s'), 'auditor_role' : _('Can view all settings for the %s'), 'execute_role' : _('May run the %s'), 'member_role' : _('User is a member of the %s'), From 6c951aa88380366f61a3aa660d448f7a855d6dc8 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 26 Jan 2018 15:27:19 +0000 Subject: [PATCH 02/10] Add Inventory Admin role --- awx/main/access.py | 4 ++-- awx/main/models/inventory.py | 2 +- awx/main/models/rbac.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 400af5e267..956a1e7802 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -641,9 +641,9 @@ class InventoryAccess(BaseAccess): def can_add(self, data): # If no data is specified, just checking for generic add permission? if not data: - return Organization.accessible_objects(self.user, 'admin_role').exists() + return Organization.accessible_objects(self.user, 'inventory_admin_role').exists() - return self.check_related('organization', Organization, data) + return self.check_related('organization', Organization, data, role_field='inventory_admin_role') @check_superuser def can_change(self, obj, data): diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 9878aab1d9..d6c8a4dbdf 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -132,7 +132,7 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin): blank=True, ) admin_role = ImplicitRoleField( - parent_role='organization.admin_role', + parent_role='organization.inventory_admin_role', ) update_role = ImplicitRoleField( parent_role='admin_role', diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 62e9348baf..2ba1bef300 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -38,6 +38,7 @@ role_names = { 'adhoc_role' : _('Ad Hoc'), 'admin_role' : _('Admin'), 'project_admin_role' : _('Project Admin'), + 'inventory_admin_role' : _('Inventory Admin'), 'auditor_role' : _('Auditor'), 'execute_role' : _('Execute'), 'member_role' : _('Member'), @@ -52,6 +53,7 @@ role_descriptions = { 'adhoc_role' : _('May run ad hoc commands on an inventory'), 'admin_role' : _('Can manage all aspects of the %s'), 'project_admin_role' : _('Can manage all projects of the %s'), + 'inventory_admin_role' : _('Can manage all inventories of the %s'), 'auditor_role' : _('Can view all settings for the %s'), 'execute_role' : _('May run the %s'), 'member_role' : _('User is a member of the %s'), From 109841c350434656615b429911d4d1a1e8672556 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 26 Jan 2018 15:43:00 +0000 Subject: [PATCH 03/10] Add Credential Admin role --- awx/main/access.py | 2 +- awx/main/models/credential/__init__.py | 2 +- awx/main/models/organization.py | 3 +++ awx/main/models/rbac.py | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 956a1e7802..dc2d7f7a84 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -991,7 +991,7 @@ class CredentialAccess(BaseAccess): def can_change(self, obj, data): if not obj: return False - return self.user in obj.admin_role and self.check_related('organization', Organization, data, obj=obj) + return self.user in obj.admin_role and self.check_related('organization', Organization, data, obj=obj, role_field='credential_admin_role') def can_delete(self, obj): # Unassociated credentials may be marked deleted by anyone, though we diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 86c3930299..0bba35b2ab 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -262,7 +262,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): admin_role = ImplicitRoleField( parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - 'organization.admin_role', + 'organization.credential_admin_role', ], ) use_role = ImplicitRoleField( diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index c82d911ecb..830b697c8a 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -49,6 +49,9 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi inventory_admin_role = ImplicitRoleField( parent_role='admin_role', ) + credential_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 2ba1bef300..3267b0206d 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -39,6 +39,7 @@ role_names = { 'admin_role' : _('Admin'), 'project_admin_role' : _('Project Admin'), 'inventory_admin_role' : _('Inventory Admin'), + 'credential_admin_role': _('Credential Admin'), 'auditor_role' : _('Auditor'), 'execute_role' : _('Execute'), 'member_role' : _('Member'), @@ -54,6 +55,7 @@ role_descriptions = { 'admin_role' : _('Can manage all aspects of the %s'), 'project_admin_role' : _('Can manage all projects of the %s'), 'inventory_admin_role' : _('Can manage all inventories of the %s'), + 'credential_admin_role': _('Can manage all credentials of the %s'), 'auditor_role' : _('Can view all settings for the %s'), 'execute_role' : _('May run the %s'), 'member_role' : _('User is a member of the %s'), From b478740f28388b9a22f6253c977ec59193c50fde Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 1 Feb 2018 15:02:02 +0000 Subject: [PATCH 04/10] Add Workflow Admin --- awx/main/access.py | 22 ++++++++++++++++------ awx/main/models/organization.py | 5 ++++- awx/main/models/rbac.py | 2 ++ awx/main/models/workflow.py | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index dc2d7f7a84..37987818c8 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -608,6 +608,7 @@ class InventoryAccess(BaseAccess): I can see inventory when: - I'm a superuser. - I'm an org admin of the inventory's org. + - I'm an inventory admin of the inventory's org. - I have read, write or admin permissions on it. I can change inventory when: - I'm a superuser. @@ -945,8 +946,12 @@ class CredentialAccess(BaseAccess): - I'm a superuser. - It's a user credential and it's my credential. - It's a user credential and I'm an admin of an organization where that - user is a member of admin of the organization. + user is a member. + - It's a user credential and I'm a credential_admin of an organization + where that user is a member. - It's a team credential and I'm an admin of the team's organization. + - It's a team credential and I'm a credential admin of the team's + organization. - It's a team credential and I'm a member of the team. I can change/delete when: - I'm a superuser. @@ -1067,6 +1072,7 @@ class ProjectAccess(BaseAccess): I can see projects when: - I am a superuser. - I am an admin in an organization associated with the project. + - I am a project admin in an organization associated with the project. - I am a user in an organization associated with the project. - I am on a team associated with the project. - I have been explicitly granted permission to run/check jobs using the @@ -1174,6 +1180,7 @@ class JobTemplateAccess(BaseAccess): a user can create a job template if - they are a superuser - an org admin of any org that the project is a member + - if they are a project_admin for any org that project is a member of - if they have user or team based permissions tying the project to the inventory source for the given action as well as the 'create' deploy permission. @@ -1725,13 +1732,14 @@ class WorkflowJobTemplateAccess(BaseAccess): Users who are able to create deploy jobs can also run normal and check (dry run) jobs. ''' if not data: # So the browseable API will work - return Organization.accessible_objects(self.user, 'admin_role').exists() + return Organization.accessible_objects(self.user, 'workflow_admin_role').exists() # will check this if surveys are added to WFJT if 'survey_enabled' in data and data['survey_enabled']: self.check_license(feature='surveys') - return self.check_related('organization', Organization, data, mandatory=True) + return self.check_related('organization', Organization, data, role_field='workflow_admin_role', + mandatory=True) def can_copy(self, obj): if self.save_messages: @@ -1758,7 +1766,8 @@ class WorkflowJobTemplateAccess(BaseAccess): if missing_inventories: self.messages['inventories_unable_to_copy'] = missing_inventories - return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True) + return self.check_related('organization', Organization, {'reference_obj': obj}, role_field='workflow_admin_role', + mandatory=True) def can_start(self, obj, validate_license=True): if validate_license: @@ -1783,7 +1792,8 @@ class WorkflowJobTemplateAccess(BaseAccess): if self.user.is_superuser: return True - return self.check_related('organization', Organization, data, obj=obj) and self.user in obj.admin_role + return (self.check_related('organization', Organization, data, role_field='workflow_admin_field', obj=obj) + and self.user in obj.admin_role) def can_delete(self, obj): is_delete_allowed = self.user.is_superuser or self.user in obj.admin_role @@ -1824,7 +1834,7 @@ class WorkflowJobAccess(BaseAccess): def can_delete(self, obj): return (obj.workflow_job_template and obj.workflow_job_template.organization and - self.user in obj.workflow_job_template.organization.admin_role) + self.user in obj.workflow_job_template.organization.workflow_admin_role) def get_method_capability(self, method, obj, parent_obj): if method == 'start': diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 830b697c8a..2898b8dc6a 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -52,11 +52,14 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi credential_admin_role = ImplicitRoleField( parent_role='admin_role', ) + workflow_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - parent_role=['admin_role', 'project_admin_role', 'inventory_admin_role'] + parent_role=['admin_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role'] ) read_role = ImplicitRoleField( parent_role=['member_role', 'auditor_role'], diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 3267b0206d..3a55de086c 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -40,6 +40,7 @@ role_names = { 'project_admin_role' : _('Project Admin'), 'inventory_admin_role' : _('Inventory Admin'), 'credential_admin_role': _('Credential Admin'), + 'workflow_admin_role' : _('Workflow Admin'), 'auditor_role' : _('Auditor'), 'execute_role' : _('Execute'), 'member_role' : _('Member'), @@ -56,6 +57,7 @@ role_descriptions = { 'project_admin_role' : _('Can manage all projects of the %s'), 'inventory_admin_role' : _('Can manage all inventories of the %s'), 'credential_admin_role': _('Can manage all credentials of the %s'), + 'workflow_admin_role' : _('Can manage all workflows of the %s'), 'auditor_role' : _('Can view all settings for the %s'), 'execute_role' : _('May run the %s'), 'member_role' : _('User is a member of the %s'), diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index e3bf2640dd..890a8806c5 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -306,7 +306,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl ) admin_role = ImplicitRoleField(parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - 'organization.admin_role' + 'organization.workflow_admin_role' ]) execute_role = ImplicitRoleField(parent_role=[ 'admin_role' From 9fdd00785f15649d83623a5d23fa6882619caf2d Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 1 Feb 2018 16:43:21 +0000 Subject: [PATCH 05/10] Add new RBAC role migrations --- awx/api/views.py | 2 +- .../migrations/0020_declare_new_rbac_roles.py | 68 +++++++++++++++++++ .../migrations/0021_create_new_rbac_roles.py | 19 ++++++ awx/main/models/jobs.py | 2 +- 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 awx/main/migrations/0020_declare_new_rbac_roles.py create mode 100644 awx/main/migrations/0021_create_new_rbac_roles.py diff --git a/awx/api/views.py b/awx/api/views.py index 23b8d0e769..1f5c633db6 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -4488,7 +4488,7 @@ class UnifiedJobTemplateList(ListAPIView): capabilities_prefetch = [ 'admin', 'execute', {'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use', - 'workflowjobtemplate.organization.admin']} + 'workflowjobtemplate.organization.workflow_admin']} ] diff --git a/awx/main/migrations/0020_declare_new_rbac_roles.py b/awx/main/migrations/0020_declare_new_rbac_roles.py new file mode 100644 index 0000000000..438733af54 --- /dev/null +++ b/awx/main/migrations/0020_declare_new_rbac_roles.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2018-02-01 16:32 +from __future__ import unicode_literals + +import awx.main.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0019_v330_custom_virtualenv'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='credential_admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), + migrations.AddField( + model_name='organization', + name='inventory_admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), + migrations.AddField( + model_name='organization', + name='project_admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), + migrations.AddField( + model_name='organization', + name='workflow_admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='credential', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'singleton:system_administrator', b'organization.credential_admin_role'], related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='inventory', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'organization.inventory_admin_role', related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='project', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'organization.project_admin_role', b'singleton:system_administrator'], related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'singleton:system_administrator', b'organization.workflow_admin_role'], related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='jobtemplate', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'project.organization.project_admin_role', b'inventory.organization.inventory_admin_role'], related_name='+', to='main.Role'), + ), + migrations.AlterField( + model_name='organization', + name='member_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role'], related_name='+', to='main.Role'), + ), + ] diff --git a/awx/main/migrations/0021_create_new_rbac_roles.py b/awx/main/migrations/0021_create_new_rbac_roles.py new file mode 100644 index 0000000000..7014f80972 --- /dev/null +++ b/awx/main/migrations/0021_create_new_rbac_roles.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations +from awx.main.migrations import ActivityStreamDisabledMigration +from awx.main.migrations import _rbac as rbac +from awx.main.migrations import _migration_utils as migration_utils + + +class Migration(ActivityStreamDisabledMigration): + + dependencies = [ + ('main', '0020_declare_new_rbac_roles'), + ] + + operations = [ + migrations.RunPython(migration_utils.set_current_apps_for_migrations), + migrations.RunPython(rbac.create_roles), + ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0d27329cdf..6626e552ae 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -270,7 +270,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour allows_field='credentials' ) admin_role = ImplicitRoleField( - parent_role=['project.organization.admin_role', 'inventory.organization.admin_role'] + parent_role=['project.organization.project_admin_role', 'inventory.organization.inventory_admin_role'] ) execute_role = ImplicitRoleField( parent_role=['admin_role'], From fbece6bdde96846e14648f725845af3d1516b122 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 2 Feb 2018 16:09:39 +0000 Subject: [PATCH 06/10] Updating and adding tests for new RBAC roles --- .../tests/functional/test_rbac_credential.py | 29 +++++++++++++++---- .../tests/functional/test_rbac_inventory.py | 7 +++-- .../functional/test_rbac_job_templates.py | 9 ++++-- .../tests/functional/test_rbac_workflow.py | 21 ++++++++++++-- awx/main/tests/unit/test_access.py | 3 +- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index b02b30755a..783fdd9e82 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -35,9 +35,31 @@ def test_credential_access_auditor(credential, organization_factory): @pytest.mark.django_db -def test_org_credential_access_member(alice, org_credential, credential): - org_credential.admin_role.members.add(alice) +def test_credential_access_member(alice, credential): credential.admin_role.members.add(alice) + access = CredentialAccess(alice) + assert access.can_change(credential, { + 'description': 'New description.', + 'organization': None}) + + +@pytest.mark.django_db +@pytest.mark.parametrize("role_name", ["admin_role", "credential_admin_role"]) +def test_org_credential_access_admin(role_name, alice, org_credential): + role = getattr(org_credential.organization, role_name) + role.members.add(alice) + + access = CredentialAccess(alice) + + # Alice should be able to PATCH if organization is not changed + assert access.can_change(org_credential, { + 'description': 'New description.', + 'organization': org_credential.organization.pk}) + + +@pytest.mark.django_db +def test_org_credential_access_member(alice, org_credential): + org_credential.admin_role.members.add(alice) access = CredentialAccess(alice) @@ -47,9 +69,6 @@ def test_org_credential_access_member(alice, org_credential, credential): 'organization': org_credential.organization.pk}) assert access.can_change(org_credential, { 'description': 'New description.'}) - assert access.can_change(credential, { - 'description': 'New description.', - 'organization': None}) @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 830e5a7b52..fd195b10b3 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -62,10 +62,13 @@ def test_org_member_inventory_script_permissions(org_member, organization): @pytest.mark.django_db -def test_access_admin(organization, inventory, user): +@pytest.mark.parametrize("role", ["admin_role", "inventory_admin_role"]) +def test_access_admin(role, organization, inventory, user): a = user('admin', False) inventory.organization = organization - organization.admin_role.members.add(a) + + role = getattr(organization, role) + role.members.add(a) access = InventoryAccess(a) assert access.can_read(inventory) diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 91778a3c5d..34a6b06e97 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -80,10 +80,15 @@ def test_job_template_access_use_level(jt_linked, rando): @pytest.mark.django_db -def test_job_template_access_org_admin(jt_linked, rando): +@pytest.mark.parametrize("role_names", [("admin_role",), ("inventory_admin_role", "project_admin_role")]) +def test_job_template_access_admin(role_names, jt_linked, rando): access = JobTemplateAccess(rando) # Appoint this user as admin of the organization - jt_linked.inventory.organization.admin_role.members.add(rando) + #jt_linked.inventory.organization.admin_role.members.add(rando) + for role_name in role_names: + role = getattr(jt_linked.inventory.organization, role_name) + role.members.add(rando) + # Assign organization permission in the same way the create view does organization = jt_linked.inventory.organization jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role) diff --git a/awx/main/tests/functional/test_rbac_workflow.py b/awx/main/tests/functional/test_rbac_workflow.py index 578db417d0..5cd63027d2 100644 --- a/awx/main/tests/functional/test_rbac_workflow.py +++ b/awx/main/tests/functional/test_rbac_workflow.py @@ -49,6 +49,13 @@ class TestWorkflowJobTemplateAccess: assert org_admin in wfjt.execute_role assert org_admin in wfjt.read_role + def test_org_workflow_admin_role_inheritance(self, wfjt, org_member): + wfjt.organization.workflow_admin_role.members.add(org_member) + + assert org_member in wfjt.admin_role + assert org_member in wfjt.execute_role + assert org_member in wfjt.read_role + @pytest.mark.django_db class TestWorkflowJobTemplateNodeAccess: @@ -103,8 +110,12 @@ class TestWorkflowJobTemplateNodeAccess: @pytest.mark.django_db class TestWorkflowJobAccess: - def test_org_admin_can_delete_workflow_job(self, workflow_job, org_admin): - access = WorkflowJobAccess(org_admin) + @pytest.mark.parametrize("role_name", ["admin_role", "workflow_admin_role"]) + def test_org_admin_can_delete_workflow_job(self, role_name, workflow_job, org_member): + role = getattr(workflow_job.workflow_job_template.organization, role_name) + role.members.add(org_member) + + access = WorkflowJobAccess(org_member) assert access.can_delete(workflow_job) def test_wfjt_admin_can_delete_workflow_job(self, workflow_job, rando): @@ -132,9 +143,13 @@ class TestWFJTCopyAccess: admin_access = WorkflowJobTemplateAccess(org_admin) assert admin_access.can_copy(wfjt) + wfjt.organization.workflow_admin_role.members.add(org_member) + admin_access = WorkflowJobTemplateAccess(org_member) + assert admin_access.can_copy(wfjt) + def test_copy_permissions_user(self, wfjt, org_admin, org_member): ''' - Only org admins are able to add WFJTs, only org admins + Only org admins and org workflow admins are able to add WFJTs, only org admins are able to copy them ''' wfjt.admin_role.members.add(org_member) diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 0692ba0490..44231daf59 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -244,8 +244,7 @@ class TestWorkflowAccessMethods: def test_workflow_can_add(self, workflow, user_unit): organization = Organization(name='test-org') workflow.organization = organization - organization.admin_role = Role() - + organization.workflow_admin_role = Role() def mock_get_object(Class, **kwargs): if Class == Organization: return organization From 9e7bd5557951e77c69aa2bff8996296fdea18305 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 2 Feb 2018 16:51:10 +0000 Subject: [PATCH 07/10] Add Notification Admin --- awx/main/access.py | 12 +++-- .../migrations/0020_declare_new_rbac_roles.py | 7 ++- awx/main/models/organization.py | 7 ++- awx/main/models/rbac.py | 54 ++++++++++--------- .../functional/test_rbac_notifications.py | 10 +++- 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 37987818c8..4d91943fec 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2218,6 +2218,7 @@ class NotificationTemplateAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( + Q(organization__in=Organization.objects.filter(notification_admin_role__members=self.user)) | Q(organization__in=self.user.admin_of_organizations) | Q(organization__in=self.user.auditor_of_organizations) ).distinct() @@ -2226,22 +2227,22 @@ class NotificationTemplateAccess(BaseAccess): if self.user.is_superuser or self.user.is_system_auditor: return True if obj.organization is not None: - if self.user in obj.organization.admin_role or self.user in obj.organization.auditor_role: + if self.user in obj.organization.notification_admin_role or self.user in obj.organization.auditor_role: return True return False @check_superuser def can_add(self, data): if not data: - return Organization.accessible_objects(self.user, 'admin_role').exists() - return self.check_related('organization', Organization, data, mandatory=True) + return Organization.accessible_objects(self.user, 'notification_admin_role').exists() + return self.check_related('organization', Organization, data, role_field='notification_admin_role', mandatory=True) @check_superuser def can_change(self, obj, data): if obj.organization is None: # only superusers are allowed to edit orphan notification templates return False - return self.check_related('organization', Organization, data, obj=obj, mandatory=True) + return self.check_related('organization', Organization, data, obj=obj, role_field='notification_admin_role', mandatory=True) def can_admin(self, obj, data): return self.can_change(obj, data) @@ -2253,7 +2254,7 @@ class NotificationTemplateAccess(BaseAccess): def can_start(self, obj, validate_license=True): if obj.organization is None: return False - return self.user in obj.organization.admin_role + return self.user in obj.organization.notification_admin_role class NotificationAccess(BaseAccess): @@ -2265,6 +2266,7 @@ class NotificationAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( + Q(notification_template__organization__in=Organization.objects.filter(notification_admin_role__members=self.user)) | Q(notification_template__organization__in=self.user.admin_of_organizations) | Q(notification_template__organization__in=self.user.auditor_of_organizations) ).distinct() diff --git a/awx/main/migrations/0020_declare_new_rbac_roles.py b/awx/main/migrations/0020_declare_new_rbac_roles.py index 438733af54..da151393ab 100644 --- a/awx/main/migrations/0020_declare_new_rbac_roles.py +++ b/awx/main/migrations/0020_declare_new_rbac_roles.py @@ -35,6 +35,11 @@ class Migration(migrations.Migration): name='workflow_admin_role', field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), ), + migrations.AddField( + model_name='organization', + name='notification_admin_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), migrations.AlterField( model_name='credential', name='admin_role', @@ -63,6 +68,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role'], related_name='+', to='main.Role'), + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role'], related_name='+', to='main.Role'), ), ] diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 2898b8dc6a..b9a562fd3c 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -55,11 +55,16 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi workflow_admin_role = ImplicitRoleField( parent_role='admin_role', ) + notification_admin_role = ImplicitRoleField( + parent_role='admin_role', + ) auditor_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - parent_role=['admin_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role'] + parent_role=['admin_role', 'project_admin_role', + 'inventory_admin_role', 'workflow_admin_role', + 'notification_admin_role'] ) read_role = ImplicitRoleField( parent_role=['member_role', 'auditor_role'], diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 3a55de086c..010d0a624b 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -33,37 +33,39 @@ ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='system_administrator' ROLE_SINGLETON_SYSTEM_AUDITOR='system_auditor' role_names = { - 'system_administrator' : _('System Administrator'), - 'system_auditor' : _('System Auditor'), - 'adhoc_role' : _('Ad Hoc'), - 'admin_role' : _('Admin'), - 'project_admin_role' : _('Project Admin'), - 'inventory_admin_role' : _('Inventory Admin'), + 'system_administrator': _('System Administrator'), + 'system_auditor': _('System Auditor'), + 'adhoc_role': _('Ad Hoc'), + 'admin_role': _('Admin'), + 'project_admin_role': _('Project Admin'), + 'inventory_admin_role': _('Inventory Admin'), 'credential_admin_role': _('Credential Admin'), - 'workflow_admin_role' : _('Workflow Admin'), - 'auditor_role' : _('Auditor'), - 'execute_role' : _('Execute'), - 'member_role' : _('Member'), - 'read_role' : _('Read'), - 'update_role' : _('Update'), - 'use_role' : _('Use'), + 'workflow_admin_role': _('Workflow Admin'), + 'notification_admin_role': _('Notification Admin'), + 'auditor_role': _('Auditor'), + 'execute_role': _('Execute'), + 'member_role': _('Member'), + 'read_role': _('Read'), + 'update_role': _('Update'), + 'use_role': _('Use'), } role_descriptions = { - 'system_administrator' : _('Can manage all aspects of the system'), - 'system_auditor' : _('Can view all settings on the system'), - 'adhoc_role' : _('May run ad hoc commands on an inventory'), - 'admin_role' : _('Can manage all aspects of the %s'), - 'project_admin_role' : _('Can manage all projects of the %s'), - 'inventory_admin_role' : _('Can manage all inventories of the %s'), + 'system_administrator': _('Can manage all aspects of the system'), + 'system_auditor': _('Can view all settings on the system'), + 'adhoc_role': _('May run ad hoc commands on an inventory'), + 'admin_role': _('Can manage all aspects of the %s'), + 'project_admin_role': _('Can manage all projects of the %s'), + 'inventory_admin_role': _('Can manage all inventories of the %s'), 'credential_admin_role': _('Can manage all credentials of the %s'), - 'workflow_admin_role' : _('Can manage all workflows of the %s'), - 'auditor_role' : _('Can view all settings for the %s'), - 'execute_role' : _('May run the %s'), - 'member_role' : _('User is a member of the %s'), - 'read_role' : _('May view settings for the %s'), - 'update_role' : _('May update project or inventory or group using the configured source update system'), - 'use_role' : _('Can use the %s in a job template'), + 'workflow_admin_role': _('Can manage all workflows of the %s'), + 'notification_admin_role': _('Can manage all notifications of the %s'), + 'auditor_role': _('Can view all settings for the %s'), + 'execute_role': _('May run the %s'), + 'member_role': _('User is a member of the %s'), + 'read_role': _('May view settings for the %s'), + 'update_role': _('May update project or inventory or group using the configured source update system'), + 'use_role': _('Can use the %s in a job template'), } diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index 80255da0d1..41fcbfc19c 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -32,6 +32,11 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user notification_template.organization.admin_role.members.add(user('admin', False)) assert access.get_queryset().count() == 1 +@pytest.mark.django_db +def test_notification_template_get_queryset_notificationadmin(notification_template, user): + access = NotificationTemplateAccess(user('admin', False)) + notification_template.organization.notification_admin_role.members.add(user('admin', False)) + assert access.get_queryset().count() == 1 @pytest.mark.django_db def test_notification_template_get_queryset_org_auditor(notification_template, org_auditor): @@ -59,12 +64,13 @@ def test_notification_template_access_superuser(notification_template_factory): @pytest.mark.django_db -def test_notification_template_access_admin(organization_factory, notification_template_factory): +@pytest.mark.parametrize("role", ["present.admin_role:admin", "present.notification_admin_role:admin"]) +def test_notification_template_access_admin(role, organization_factory, notification_template_factory): other_objects = organization_factory('other') present_objects = organization_factory('present', users=['admin'], notification_templates=['test-notification'], - roles=['present.admin_role:admin']) + roles=[role]) notification_template = present_objects.notification_templates.test_notification other_org = other_objects.organization From 819b318fe560625b299f31b1577bf75575d7b623 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 2 Feb 2018 17:25:33 +0000 Subject: [PATCH 08/10] Add Org Execute --- awx/main/access.py | 4 ++-- .../migrations/0020_declare_new_rbac_roles.py | 17 ++++++++++++++++- awx/main/models/jobs.py | 2 +- awx/main/models/organization.py | 5 ++++- awx/main/models/workflow.py | 3 ++- .../tests/functional/test_rbac_notifications.py | 2 ++ awx/main/tests/unit/test_access.py | 1 + 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 4d91943fec..1bc1d90f5f 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1792,8 +1792,8 @@ class WorkflowJobTemplateAccess(BaseAccess): if self.user.is_superuser: return True - return (self.check_related('organization', Organization, data, role_field='workflow_admin_field', obj=obj) - and self.user in obj.admin_role) + return (self.check_related('organization', Organization, data, role_field='workflow_admin_field', obj=obj) and + self.user in obj.admin_role) def can_delete(self, obj): is_delete_allowed = self.user.is_superuser or self.user in obj.admin_role diff --git a/awx/main/migrations/0020_declare_new_rbac_roles.py b/awx/main/migrations/0020_declare_new_rbac_roles.py index da151393ab..9b489b7b70 100644 --- a/awx/main/migrations/0020_declare_new_rbac_roles.py +++ b/awx/main/migrations/0020_declare_new_rbac_roles.py @@ -15,6 +15,11 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name='organization', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=b'admin_role', related_name='+', to='main.Role'), + ), migrations.AddField( model_name='organization', name='credential_admin_role', @@ -60,14 +65,24 @@ class Migration(migrations.Migration): name='admin_role', field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'singleton:system_administrator', b'organization.workflow_admin_role'], related_name='+', to='main.Role'), ), + migrations.AlterField( + model_name='workflowjobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'organization.execute_role'], related_name='+', to='main.Role'), + ), migrations.AlterField( model_name='jobtemplate', name='admin_role', field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'project.organization.project_admin_role', b'inventory.organization.inventory_admin_role'], related_name='+', to='main.Role'), ), + migrations.AlterField( + model_name='jobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project.organization.execute_role', b'inventory.organization.execute_role'], related_name='+', to='main.Role'), + ), migrations.AlterField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role'], related_name='+', to='main.Role'), + field=awx.main.fields.ImplicitRoleField(null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role', b'execute_role'], related_name='+', to='main.Role'), ), ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 6626e552ae..5efd502a7d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -273,7 +273,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour parent_role=['project.organization.project_admin_role', 'inventory.organization.inventory_admin_role'] ) execute_role = ImplicitRoleField( - parent_role=['admin_role'], + parent_role=['admin_role', 'project.organization.execute_role', 'inventory.organization.execute_role'], ) read_role = ImplicitRoleField( parent_role=['project.organization.auditor_role', 'inventory.organization.auditor_role', 'execute_role', 'admin_role'], diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index b9a562fd3c..fef640182a 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -43,6 +43,9 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi admin_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ) + execute_role = ImplicitRoleField( + parent_role='admin_role', + ) project_admin_role = ImplicitRoleField( parent_role='admin_role', ) @@ -62,7 +65,7 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - parent_role=['admin_role', 'project_admin_role', + parent_role=['admin_role', 'execute_role', 'project_admin_role', 'inventory_admin_role', 'workflow_admin_role', 'notification_admin_role'] ) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 890a8806c5..5c582683c6 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -309,7 +309,8 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl 'organization.workflow_admin_role' ]) execute_role = ImplicitRoleField(parent_role=[ - 'admin_role' + 'admin_role', + 'organization.execute_role', ]) read_role = ImplicitRoleField(parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index 41fcbfc19c..18ff3959aa 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -32,12 +32,14 @@ def test_notification_template_get_queryset_orgadmin(notification_template, user notification_template.organization.admin_role.members.add(user('admin', False)) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_get_queryset_notificationadmin(notification_template, user): access = NotificationTemplateAccess(user('admin', False)) notification_template.organization.notification_admin_role.members.add(user('admin', False)) assert access.get_queryset().count() == 1 + @pytest.mark.django_db def test_notification_template_get_queryset_org_auditor(notification_template, org_auditor): access = NotificationTemplateAccess(org_auditor) diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 44231daf59..6b20aaaed9 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -245,6 +245,7 @@ class TestWorkflowAccessMethods: organization = Organization(name='test-org') workflow.organization = organization organization.workflow_admin_role = Role() + def mock_get_object(Class, **kwargs): if Class == Organization: return organization From 13e777f01bb01d255e037bb2c01f5bd81d375bc2 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 2 Feb 2018 18:31:28 +0000 Subject: [PATCH 09/10] Rename migration files --- ...re_new_rbac_roles.py => 0020_v330_declare_new_rbac_roles.py} | 0 ...ate_new_rbac_roles.py => 0021_v330_create_new_rbac_roles.py} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0020_declare_new_rbac_roles.py => 0020_v330_declare_new_rbac_roles.py} (100%) rename awx/main/migrations/{0021_create_new_rbac_roles.py => 0021_v330_create_new_rbac_roles.py} (90%) diff --git a/awx/main/migrations/0020_declare_new_rbac_roles.py b/awx/main/migrations/0020_v330_declare_new_rbac_roles.py similarity index 100% rename from awx/main/migrations/0020_declare_new_rbac_roles.py rename to awx/main/migrations/0020_v330_declare_new_rbac_roles.py diff --git a/awx/main/migrations/0021_create_new_rbac_roles.py b/awx/main/migrations/0021_v330_create_new_rbac_roles.py similarity index 90% rename from awx/main/migrations/0021_create_new_rbac_roles.py rename to awx/main/migrations/0021_v330_create_new_rbac_roles.py index 7014f80972..fe15c951dd 100644 --- a/awx/main/migrations/0021_create_new_rbac_roles.py +++ b/awx/main/migrations/0021_v330_create_new_rbac_roles.py @@ -10,7 +10,7 @@ from awx.main.migrations import _migration_utils as migration_utils class Migration(ActivityStreamDisabledMigration): dependencies = [ - ('main', '0020_declare_new_rbac_roles'), + ('main', '0020_v330_declare_new_rbac_roles'), ] operations = [ From 30a5617825604504d212ca7355ba503843f813ce Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 14 Feb 2018 22:06:10 +0000 Subject: [PATCH 10/10] Address PR feedback --- awx/main/access.py | 15 +++++++------ ...py => 0021_v330_declare_new_rbac_roles.py} | 2 +- ....py => 0022_v330_create_new_rbac_roles.py} | 2 +- awx/main/models/rbac.py | 21 +++++++++++++++---- 4 files changed, 26 insertions(+), 14 deletions(-) rename awx/main/migrations/{0020_v330_declare_new_rbac_roles.py => 0021_v330_declare_new_rbac_roles.py} (98%) rename awx/main/migrations/{0021_v330_create_new_rbac_roles.py => 0022_v330_create_new_rbac_roles.py} (90%) diff --git a/awx/main/access.py b/awx/main/access.py index 1bc1d90f5f..7e5b3b281a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -660,7 +660,7 @@ class InventoryAccess(BaseAccess): # Verify that the user has access to the new organization if moving an # inventory to a new organization. Otherwise, just check for admin permission. return ( - self.check_related('organization', Organization, data, obj=obj, + self.check_related('organization', Organization, data, obj=obj, role_field='inventory_admin_role', mandatory=org_admin_mandatory) and self.user in obj.admin_role ) @@ -985,7 +985,8 @@ class CredentialAccess(BaseAccess): return check_user_access(self.user, Team, 'change', team_obj, None) if data and data.get('organization', None): organization_obj = get_object_from_data('organization', Organization, data) - return check_user_access(self.user, Organization, 'change', organization_obj, None) + return any([check_user_access(self.user, Organization, 'change', organization_obj, None), + self.user in organization_obj.credential_admin_role]) return False @check_superuser @@ -1098,7 +1099,7 @@ class ProjectAccess(BaseAccess): @check_superuser def can_change(self, obj, data): - if not self.check_related('organization', Organization, data, obj=obj): + if not self.check_related('organization', Organization, data, obj=obj, role_field='project_admin_role'): return False return self.user in obj.admin_role @@ -1439,7 +1440,7 @@ class JobAccess(BaseAccess): elif not jt_access: return False - org_access = obj.inventory and self.user in obj.inventory.organization.admin_role + org_access = obj.inventory and self.user in obj.inventory.organization.inventory_admin_role project_access = obj.project is None or self.user in obj.project.admin_role credential_access = all([self.user in cred.use_role for cred in obj.credentials.all()]) @@ -2218,8 +2219,7 @@ class NotificationTemplateAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(organization__in=Organization.objects.filter(notification_admin_role__members=self.user)) | - Q(organization__in=self.user.admin_of_organizations) | + Q(organization__in=Organization.accessible_objects(self.user, 'notification_admin_role')) | Q(organization__in=self.user.auditor_of_organizations) ).distinct() @@ -2266,8 +2266,7 @@ class NotificationAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(notification_template__organization__in=Organization.objects.filter(notification_admin_role__members=self.user)) | - Q(notification_template__organization__in=self.user.admin_of_organizations) | + Q(notification_template__organization__in=Organization.accessible_objects(self.user, 'notification_admin_role')) | Q(notification_template__organization__in=self.user.auditor_of_organizations) ).distinct() diff --git a/awx/main/migrations/0020_v330_declare_new_rbac_roles.py b/awx/main/migrations/0021_v330_declare_new_rbac_roles.py similarity index 98% rename from awx/main/migrations/0020_v330_declare_new_rbac_roles.py rename to awx/main/migrations/0021_v330_declare_new_rbac_roles.py index 9b489b7b70..a279ef90e3 100644 --- a/awx/main/migrations/0020_v330_declare_new_rbac_roles.py +++ b/awx/main/migrations/0021_v330_declare_new_rbac_roles.py @@ -11,7 +11,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('main', '0019_v330_custom_virtualenv'), + ('main', '0020_v330_instancegroup_policies'), ] operations = [ diff --git a/awx/main/migrations/0021_v330_create_new_rbac_roles.py b/awx/main/migrations/0022_v330_create_new_rbac_roles.py similarity index 90% rename from awx/main/migrations/0021_v330_create_new_rbac_roles.py rename to awx/main/migrations/0022_v330_create_new_rbac_roles.py index fe15c951dd..578be8645c 100644 --- a/awx/main/migrations/0021_v330_create_new_rbac_roles.py +++ b/awx/main/migrations/0022_v330_create_new_rbac_roles.py @@ -10,7 +10,7 @@ from awx.main.migrations import _migration_utils as migration_utils class Migration(ActivityStreamDisabledMigration): dependencies = [ - ('main', '0020_v330_declare_new_rbac_roles'), + ('main', '0021_v330_declare_new_rbac_roles'), ] operations = [ diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 010d0a624b..930b226ac6 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -61,7 +61,10 @@ role_descriptions = { 'workflow_admin_role': _('Can manage all workflows of the %s'), 'notification_admin_role': _('Can manage all notifications of the %s'), 'auditor_role': _('Can view all settings for the %s'), - 'execute_role': _('May run the %s'), + 'execute_role': { + 'organization': _('May run any executable resources in the organization'), + 'default': _('May run the %s'), + }, 'member_role': _('User is a member of the %s'), 'read_role': _('May view settings for the %s'), 'update_role': _('May update project or inventory or group using the configured source update system'), @@ -180,12 +183,22 @@ class Role(models.Model): global role_descriptions description = role_descriptions[self.role_field] content_type = self.content_type - if '%s' in description and content_type: + + model_name = None + if content_type: model = content_type.model_class() model_name = re.sub(r'([a-z])([A-Z])', r'\1 \2', model.__name__).lower() - description = description % model_name - return description + value = description + if type(description) == dict: + value = description.get(model_name) + if value is None: + value = description.get('default') + + if '%s' in value and content_type: + value = value % model_name + + return value @staticmethod def rebuild_role_ancestor_list(additions, removals):