From 8887db231b1930e030bc79b296b95f21fa086bb1 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Apr 2016 09:44:20 -0400 Subject: [PATCH 01/56] Progress on ripping out RolePermissions --- awx/main/access.py | 1 - awx/main/fields.py | 37 +------ awx/main/models/__init__.py | 1 - awx/main/models/credential.py | 3 - awx/main/models/inventory.py | 12 -- awx/main/models/jobs.py | 3 - awx/main/models/mixins.py | 6 +- awx/main/models/organization.py | 7 -- awx/main/models/projects.py | 4 - awx/main/models/rbac.py | 115 +------------------- awx/main/tests/functional/test_rbac_core.py | 36 ++---- 11 files changed, 16 insertions(+), 209 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 6ea07c318b..3305dc8983 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -18,7 +18,6 @@ from rest_framework.exceptions import ParseError, PermissionDenied from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.models.mixins import ResourceMixin -from awx.main.models.rbac import ALL_PERMISSIONS from awx.api.license import LicenseForbids from awx.main.task_engine import TaskSerializer from awx.main.conf import tower_settings diff --git a/awx/main/fields.py b/awx/main/fields.py index 7e57c29c7d..30bd8e03f3 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -93,10 +93,9 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): class ImplicitRoleField(models.ForeignKey): """Implicitly creates a role entry for a resource""" - def __init__(self, role_name=None, role_description=None, permissions=None, parent_role=None, *args, **kwargs): + def __init__(self, role_name=None, role_description=None, parent_role=None, *args, **kwargs): self.role_name = role_name self.role_description = role_description if role_description else "" - self.permissions = permissions self.parent_role = parent_role kwargs.setdefault('to', 'Role') @@ -108,7 +107,6 @@ class ImplicitRoleField(models.ForeignKey): name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct() kwargs['role_name'] = self.role_name kwargs['role_description'] = self.role_description - kwargs['permissions'] = self.permissions kwargs['parent_role'] = self.parent_role return name, path, args, kwargs @@ -190,40 +188,11 @@ class ImplicitRoleField(models.ForeignKey): ) setattr(instance, self.name, role) - def _patch_role_content_object_and_grant_permissions(self, instance): + def _patch_role_content_object(self, instance): role = getattr(instance, self.name) role.content_object = instance role.save() - if self.permissions is not None: - RolePermission_ = get_current_apps().get_model('main', 'RolePermission') - ContentType = get_current_apps().get_model('contenttypes', "ContentType") - instance_content_type = ContentType.objects.get_for_model(instance) - - permissions = RolePermission_( - created=now(), - modified=now(), - role=role, - content_type=instance_content_type, - object_id=instance.id, - auto_generated=True - ) - - if 'all' in self.permissions and self.permissions['all']: - del self.permissions['all'] - self.permissions['create'] = True - self.permissions['read'] = True - self.permissions['write'] = True - self.permissions['update'] = True - self.permissions['delete'] = True - self.permissions['scm_update'] = True - self.permissions['use'] = True - self.permissions['execute'] = True - - for k,v in self.permissions.items(): - setattr(permissions, k, v) - permissions.save() - def _pre_save(self, instance, *args, **kwargs): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): implicit_role_field._create_role_instance_if_not_exists(instance) @@ -231,7 +200,7 @@ class ImplicitRoleField(models.ForeignKey): def _post_save(self, instance, created, *args, **kwargs): if created: for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): - implicit_role_field._patch_role_content_object_and_grant_permissions(instance) + implicit_role_field._patch_role_content_object(instance) with batch_role_ancestor_rebuilding(): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 847f52bb35..fef22b5e2d 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -46,7 +46,6 @@ User.add_to_class('can_access', check_user_access) User.add_to_class('accessible_by', user_accessible_by) User.add_to_class('accessible_objects', user_accessible_objects) User.add_to_class('admin_role', user_admin_role) -User.add_to_class('role_permissions', GenericRelation('main.RolePermission')) @property def user_get_organizations(user): diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index d247154fd7..1b20476c58 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -174,7 +174,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Credential Auditor', @@ -182,12 +181,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], - permissions = {'read': True} ) usage_role = ImplicitRoleField( role_name='Credential User', role_description='May use this credential, but not read sensitive portions or modify it', - permissions = {'use': True} ) @property diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 2af823885d..90515c2895 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -100,28 +100,23 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory Administrator', role_description='May manage this inventory', parent_role='organization.admin_role', - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Inventory Auditor', role_description='May view but not modify this inventory', parent_role='organization.auditor_role', - permissions = {'read': True} ) updater_role = ImplicitRoleField( role_name='Inventory Updater', role_description='May update the inventory', - permissions = {'read': True, 'update': True} ) usage_role = ImplicitRoleField( role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', - permissions = {'use': True} ) executor_role = ImplicitRoleField( role_name='Inventory Executor', role_description='May execute jobs against this inventory', - permissions = {'read': True, 'execute': True} ) def get_absolute_url(self): @@ -525,22 +520,18 @@ class Group(CommonModelNameNotUnique, ResourceMixin): admin_role = ImplicitRoleField( role_name='Inventory Group Administrator', parent_role=['inventory.admin_role', 'parents.admin_role'], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Inventory Group Auditor', parent_role=['inventory.auditor_role', 'parents.auditor_role'], - permissions = {'read': True} ) updater_role = ImplicitRoleField( role_name='Inventory Group Updater', parent_role=['inventory.updater_role', 'parents.updater_role'], - permissions = {'read': True, 'write': True, 'create': True, 'use': True}, ) executor_role = ImplicitRoleField( role_name='Inventory Group Executor', parent_role=['inventory.executor_role', 'parents.executor_role'], - permissions = {'read':True, 'execute':True}, ) def __unicode__(self): @@ -1296,21 +1287,18 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): role_name='CustomInventory Administrator', role_description='May manage this inventory', parent_role='organization.admin_role', - permissions = {'all': True} ) member_role = ImplicitRoleField( role_name='CustomInventory Member', role_description='May view but not modify this inventory', parent_role='organization.member_role', - permissions = {'read': True} ) auditor_role = ImplicitRoleField( role_name='CustomInventory Auditor', role_description='May view but not modify this inventory', parent_role='organization.auditor_role', - permissions = {'read': True} ) def get_absolute_url(self): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0314a57fb7..d9747f71cf 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -207,18 +207,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): role_name='Job Template Administrator', role_description='Full access to all settings', parent_role='project.admin_role', - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Job Template Auditor', role_description='Read-only access to all settings', parent_role='project.auditor_role', - permissions = {'read': True} ) executor_role = ImplicitRoleField( role_name='Job Template Runner', role_description='May run the job template', - permissions = {'read': True, 'execute': True} ) @classmethod diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index bcf95fa4ac..1396698c28 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -7,8 +7,6 @@ from django.contrib.auth.models import User # noqa # AWX from awx.main.models.rbac import ( - get_user_permissions_on_resource, - get_role_permissions_on_resource, Role, ) @@ -20,10 +18,8 @@ class ResourceMixin(models.Model): class Meta: abstract = True - role_permissions = GenericRelation('main.RolePermission') - @classmethod - def accessible_objects(cls, accessor, permissions): + def accessible_objects(cls, accessor, role_name): ''' Use instead of `MyModel.objects` when you want to only consider resources that a user has specific permissions for. For example: diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 615a9104fe..f497d2a120 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -19,7 +19,6 @@ from django.utils.translation import ugettext_lazy as _ from awx.main.fields import AutoOneToOneField, ImplicitRoleField from awx.main.models.base import * # noqa from awx.main.models.rbac import ( - ALL_PERMISSIONS, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR, ) @@ -57,19 +56,16 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): role_name='Organization Administrator', role_description='May manage all aspects of this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - permissions = ALL_PERMISSIONS, ) auditor_role = ImplicitRoleField( role_name='Organization Auditor', role_description='May read all settings associated with this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Organization Member', role_description='A member of this organization', parent_role='admin_role', - permissions = {'read': True} ) @@ -112,19 +108,16 @@ class Team(CommonModelNameNotUnique, ResourceMixin): role_name='Team Administrator', role_description='May manage this team', parent_role='organization.admin_role', - permissions = ALL_PERMISSIONS, ) auditor_role = ImplicitRoleField( role_name='Team Auditor', role_description='May read all settings associated with this team', parent_role='organization.auditor_role', - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Team Member', role_description='A member of this team', parent_role='admin_role', - permissions = {'read':True}, ) def get_absolute_url(self): diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index e5d1d58d19..0f023ab56b 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -227,7 +227,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): 'organization.admin_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Project Auditor', @@ -236,18 +235,15 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): 'organization.auditor_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Project Member', role_description='Implies membership within this project', - permissions = {'read': True} ) scm_update_role = ImplicitRoleField( role_name='Project Updater', role_description='May update this project from the source control management system', parent_role='admin_role', - permissions = {'scm_update': True} ) @classmethod diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 91e055f6eb..2d7b5d279e 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -21,10 +21,7 @@ from awx.main.models.base import * # noqa __all__ = [ 'Role', - 'RolePermission', 'batch_role_ancestor_rebuilding', - 'get_user_permissions_on_resource', - 'get_role_permissions_on_resource', 'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR', 'ROLE_SINGLETON_SYSTEM_AUDITOR', ] @@ -34,10 +31,6 @@ logger = logging.getLogger('awx.main.models.rbac') ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator' ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor' -ALL_PERMISSIONS = {'create': True, 'read': True, 'update': True, 'delete': True, - 'write': True, 'scm_update': True, 'use': True, 'execute': True} - - tls = threading.local() # thread local storage @contextlib.contextmanager @@ -97,6 +90,8 @@ class Role(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) + def __contains__(self, user): + return self.ancestors.filter(members=user).exists() def rebuild_role_ancestor_list(self): ''' @@ -143,109 +138,3 @@ class Role(CommonModelNameNotUnique): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() - -class RolePermission(CreatedModifiedModel): - ''' - Defines the permissions a role has - ''' - - class Meta: - app_label = 'main' - verbose_name_plural = _('permissions') - db_table = 'main_rbac_permissions' - index_together = [ - ('content_type', 'object_id') - ] - - role = models.ForeignKey( - Role, - null=False, - on_delete=models.CASCADE, - related_name='permissions', - ) - content_type = models.ForeignKey(ContentType, null=False, default=None) - object_id = models.PositiveIntegerField(null=False, default=None) - resource = GenericForeignKey('content_type', 'object_id') - auto_generated = models.BooleanField(default=False) - - create = models.IntegerField(default = 0) - read = models.IntegerField(default = 0) - write = models.IntegerField(default = 0) - delete = models.IntegerField(default = 0) - update = models.IntegerField(default = 0) - execute = models.IntegerField(default = 0) - scm_update = models.IntegerField(default = 0) - use = models.IntegerField(default = 0) - - - -def get_user_permissions_on_resource(resource, user): - ''' - Returns a dict (or None) of the permissions a user has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to the roles that are applicable for the given - user. - - In example, if a user has been granted read access through a permission - on one role and write access through a permission on a separate role, - the returned dict will denote that the user has both read and write - access. - ''' - - if type(user) == User: - roles = user.roles.all() - else: - accessor_type = ContentType.objects.get_for_model(user) - roles = Role.objects.filter(content_type__pk=accessor_type.id, - object_id=user.id) - - qs = RolePermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource), - object_id=resource.id, - role__ancestors__in=roles, - ) - - res = qs = qs.aggregate( - create = Max('create'), - read = Max('read'), - write = Max('write'), - update = Max('update'), - delete = Max('delete'), - scm_update = Max('scm_update'), - execute = Max('execute'), - use = Max('use') - ) - if res['read'] is None: - return None - return res - -def get_role_permissions_on_resource(resource, role): - ''' - Returns a dict (or None) of the permissions a role has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to either the role or any descendents of that role. - ''' - - qs = RolePermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource), - object_id=resource.id, - role__ancestors=role - ) - - res = qs = qs.aggregate( - create = Max('create'), - read = Max('read'), - write = Max('write'), - update = Max('update'), - delete = Max('delete'), - scm_update = Max('scm_update'), - execute = Max('execute'), - use = Max('use') - ) - if res['read'] is None: - return None - return res diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 2ad1250f81..f381ddd030 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -2,7 +2,6 @@ import pytest from awx.main.models import ( Role, - RolePermission, Organization, Project, ) @@ -14,23 +13,21 @@ def test_auto_inheritance_by_children(organization, alice): B = Role.objects.create(name='B') A.members.add(alice) - - - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 A.children.add(B) - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 A.children.add(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is True - assert Organization.accessible_objects(alice, {'read': True}).count() == 1 + assert alice in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 1 A.children.remove(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role B.children.add(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role B.children.remove(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 # We've had the case where our pre/post save init handlers in our field descriptors # end up creating a ton of role objects because of various not-so-obvious issues @@ -56,19 +53,6 @@ def test_auto_inheritance_by_parents(organization, alice): assert organization.accessible_by(alice, {'read': True}) is False -@pytest.mark.django_db -def test_permission_union(organization, alice): - A = Role.objects.create(name='A') - A.members.add(alice) - B = Role.objects.create(name='B') - B.members.add(alice) - - assert organization.accessible_by(alice, {'read': True, 'write': True}) is False - RolePermission.objects.create(role=A, resource=organization, read=True) - assert organization.accessible_by(alice, {'read': True, 'write': True}) is False - RolePermission.objects.create(role=A, resource=organization, write=True) - assert organization.accessible_by(alice, {'read': True, 'write': True}) is True - @pytest.mark.django_db def test_accessible_objects(organization, alice, bob): From 25f0d65c5ff639bf51eeb6dc613cb928beb58d73 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 14 Apr 2016 21:56:10 -0400 Subject: [PATCH 02/56] Ancestor rebuild optimization progress --- .../management/commands/inventory_import.py | 73 +++--- awx/main/models/rbac.py | 225 +++++++++++++++++- awx/main/tests/functional/test_rbac_core.py | 98 +++++++- .../tests/old/commands/commands_monolithic.py | 2 +- config/awx-munin.conf | 1 - 5 files changed, 349 insertions(+), 50 deletions(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 91b3a0a544..47d20cc1dd 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -780,10 +780,10 @@ class Command(NoArgsCommand): if settings.SQL_DEBUG: queries_before = len(connection.queries) if self.inventory_source.group: - groups_qs = self.inventory_source.group.all_children + groups_qs = self.inventory_source.group.all_children.all() # FIXME: Also include groups from inventory_source.managed_groups? else: - groups_qs = self.inventory.groups + groups_qs = self.inventory.groups.all() # Build list of all group pks, remove those that should not be deleted. del_group_pks = set(groups_qs.values_list('pk', flat=True)) all_group_names = self.all_group.all_groups.keys() @@ -1273,42 +1273,43 @@ class Command(NoArgsCommand): self.is_custom) self.all_group.debug_tree() - # Ensure that this is managed as an atomic SQL transaction, - # and thus properly rolled back if there is an issue. - with transaction.atomic(): - # Merge/overwrite inventory into database. - if settings.SQL_DEBUG: - self.logger.warning('loading into database...') - with ignore_inventory_computed_fields(): - if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): - self.load_into_database() - else: - with disable_activity_stream(): + with batch_role_ancestor_rebuilding(): + # Ensure that this is managed as an atomic SQL transaction, + # and thus properly rolled back if there is an issue. + with transaction.atomic(): + # Merge/overwrite inventory into database. + if settings.SQL_DEBUG: + self.logger.warning('loading into database...') + with ignore_inventory_computed_fields(): + if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): self.load_into_database() - if settings.SQL_DEBUG: - queries_before2 = len(connection.queries) - self.inventory.update_computed_fields() - if settings.SQL_DEBUG: - self.logger.warning('update computed fields took %d queries', - len(connection.queries) - queries_before2) - try: - self.check_license() - except CommandError as e: - self.mark_license_failure(save=True) - raise e + else: + with disable_activity_stream(): + self.load_into_database() + if settings.SQL_DEBUG: + queries_before2 = len(connection.queries) + self.inventory.update_computed_fields() + if settings.SQL_DEBUG: + self.logger.warning('update computed fields took %d queries', + len(connection.queries) - queries_before2) + try: + self.check_license() + except CommandError as e: + self.mark_license_failure(save=True) + raise e - if self.inventory_source.group: - inv_name = 'group "%s"' % (self.inventory_source.group.name) - else: - inv_name = '"%s" (id=%s)' % (self.inventory.name, - self.inventory.id) - if settings.SQL_DEBUG: - self.logger.warning('Inventory import completed for %s in %0.1fs', - inv_name, time.time() - begin) - else: - self.logger.info('Inventory import completed for %s in %0.1fs', - inv_name, time.time() - begin) - status = 'successful' + if self.inventory_source.group: + inv_name = 'group "%s"' % (self.inventory_source.group.name) + else: + inv_name = '"%s" (id=%s)' % (self.inventory.name, + self.inventory.id) + if settings.SQL_DEBUG: + self.logger.warning('Inventory import completed for %s in %0.1fs', + inv_name, time.time() - begin) + else: + self.logger.info('Inventory import completed for %s in %0.1fs', + inv_name, time.time() - begin) + status = 'successful' # If we're in debug mode, then log the queries and time # used to do the operation. diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 91e055f6eb..f2ff1956ca 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -7,7 +7,7 @@ import threading import contextlib # Django -from django.db import models, transaction +from django.db import models, transaction, connection from django.db.models import Q from django.db.models.aggregates import Max from django.core.urlresolvers import reverse @@ -65,9 +65,11 @@ def batch_role_ancestor_rebuilding(allow_nesting=False): if not batch_role_rebuilding: rebuild_set = getattr(tls, 'roles_needing_rebuilding') with transaction.atomic(): - for role in Role.objects.filter(id__in=list(rebuild_set)).all(): - # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this - role.rebuild_role_ancestor_list() + Role._simultaneous_ancestry_rebuild(rebuild_set) + + #for role in Role.objects.filter(id__in=list(rebuild_set)).all(): + # # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this + # role.rebuild_role_ancestor_list() delattr(tls, 'roles_needing_rebuilding') @@ -97,7 +99,6 @@ class Role(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) - def rebuild_role_ancestor_list(self): ''' Updates our `ancestors` map to accurately reflect all of the ancestors for a role @@ -115,12 +116,13 @@ class Role(CommonModelNameNotUnique): roles_needing_rebuilding.add(self.id) return - actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True)) - actual_ancestors.add(self.id) - if None in actual_ancestors: - actual_ancestors.remove(None) - stored_ancestors = set(self.ancestors.all().values_list('id', flat=True)) + #actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True)) + #actual_ancestors.add(self.id) + #if None in actual_ancestors: + # actual_ancestors.remove(None) + #stored_ancestors = set(self.ancestors.all().values_list('id', flat=True)) + ''' # If it differs, update, and then update all of our children if actual_ancestors != stored_ancestors: for id in actual_ancestors - stored_ancestors: @@ -130,6 +132,209 @@ class Role(CommonModelNameNotUnique): for child in self.children.all(): child.rebuild_role_ancestor_list() + ''' + + # If our role heirarchy hasn't actually changed, don't do anything + #if actual_ancestors == stored_ancestors: + # return + + Role._simultaneous_ancestry_rebuild([self.id]) + + + @staticmethod + def _simultaneous_ancestry_rebuild2(role_ids_to_rebuild): + #all_parents = Role.parents.through.values_list('to_role_id', 'from_role_id') + + ''' + sql_params = { + 'ancestors_table': Role.ancestors.through._meta.db_table, + 'parents_table': Role.parents.through._meta.db_table, + 'roles_table': Role._meta.db_table, + 'ids': ','.join(str(x) for x in role_ids_to_rebuild) + } + ''' + #Expand our parent list + #Expand our child list + #Construct our ancestor list for + #apply diff + + + + @staticmethod + def _simultaneous_ancestry_rebuild(role_ids_to_rebuild): + # + # The simple version of what this function is doing + # ================================================= + # + # When something changes in our role "hierarchy", we need to update + # the `Role.ancestors` mapping to reflect these changes. The basic + # idea, which the code in this method is modeled after, is to do + # this: When a change happens to a role's parents list, we update + # that role's ancestry list, then we recursively update any child + # roles ancestry lists. Because our role relationships are not + # strictly hierarchical, and can even have loops, this process may + # necessarily visit the same nodes more than once. To handle this + # without having to keep track of what should be updated (again) and + # in what order, we simply use the termination condition of stopping + # when our stored ancestry list matches what our list should be, eg, + # when nothing changes. This can be simply implemented: + # + # if actual_ancestors != stored_ancestors: + # for id in actual_ancestors - stored_ancestors: + # self.ancestors.add(id) + # for id in stored_ancestors - actual_ancestors: + # self.ancestors.remove(id) + # + # for child in self.children.all(): + # child.rebuild_role_ancestor_list() + # + # However this results in a lot of calls to the database, so the + # optimized implementation below effectively does this same thing, + # but we update all children at once, so effectively we sweep down + # through our hierarchy one layer at a time instead of one node at a + # time. Because of how this method works, we can also start from many + # roots at once and sweep down a large set of roles, which we take + # advantage of when performing bulk operations. + # + # + # SQL Breakdown + # ============= + # The Role ancestors has three columns, (id, from_role_id, to_role_id) + # + # id: Unqiue row ID + # from_role_id: Descendent role ID + # to_role_id: Ancestor role ID + # + # *NOTE* In addition to mapping roles to parents, there also + # always exists must exist an entry where + # + # from_role_id == role_id == to_role_id + # + # this makes our joins simple when we go to derive permissions or + # accessible objects. + # + # + # We operate under the assumption that our parent's ancestor list is + # correct, thus we can always compute what our ancestor list should + # be by taking the union of our parent's ancestor lists and adding + # our self reference entry from_role_id == role_id == to_role_id + # + # The inner query for the two SQL statements compute this union, + # the union of the parent's ancestors and the self referncing entry, + # for all roles in the current set of roles to rebuild. + # + # The DELETE query uses this to select all entries on disk for the + # roles we're dealing with, and removes the entries that are not in + # this list. + # + # The INSERT query uses this to select all entries in the list that + # are not in the database yet, and inserts all of the missing + # records. + # + # Once complete, we select all of the children for the roles we are + # working with, this list becomes the new role list we are working + # with. + # + # When our delete or insert query return that they have not performed + # any work, then we know that our children will also not need to be + # updated, and so we can terminate our loop. + # + # + # *NOTE* Keen reader may realize that there are many instances where + # fuck. cycles will never shake parents. + # + # + + cursor = connection.cursor() + loop_ct = 0 + + sql_params = { + 'ancestors_table': Role.ancestors.through._meta.db_table, + 'parents_table': Role.parents.through._meta.db_table, + 'roles_table': Role._meta.db_table, + 'ids': ','.join(str(x) for x in role_ids_to_rebuild) + } + + # This is our solution for dealing with updates to parents of nodes + # that are in cycles. It seems like we should be able to find a more + # clever way of just dealing with the issue in another way, but it's + # surefire and I'm not seeing the easy solution to dealing with that + # problem that's not this. + + # TODO: Test to see if not deleting any entry that has a direct + # correponding entry in the parents table helps reduce the processing + # time significantly + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE to_role_id IN (%(ids)s) + AND from_role_id != to_role_id + ''' % sql_params) + + + while role_ids_to_rebuild: + if loop_ct > 1000: + raise Exception('Ancestry role rebuilding error: infinite loop detected') + loop_ct += 1 + + sql_params = { + 'ancestors_table': Role.ancestors.through._meta.db_table, + 'parents_table': Role.parents.through._meta.db_table, + 'roles_table': Role._meta.db_table, + 'ids': ','.join(str(x) for x in role_ids_to_rebuild) + } + + delete_ct = 0 + + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE from_role_id IN (%(ids)s) + AND + id NOT IN ( + SELECT %(ancestors_table)s.id FROM ( + SELECT parents.from_role_id from_id, ancestors.to_role_id to_id + FROM %(parents_table)s as parents + LEFT JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.from_role_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.to_role_id IS NOT NULL + + UNION + + SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.from_role_id + AND new_ancestry_list.to_id = %(ancestors_table)s.to_role_id) + WHERE %(ancestors_table)s.id IS NOT NULL + ) + ''' % sql_params) + delete_ct = cursor.rowcount + + cursor.execute(''' + INSERT INTO %(ancestors_table)s (from_role_id, to_role_id) + SELECT from_id, to_id FROM ( + SELECT parents.from_role_id from_id, ancestors.to_role_id to_id + FROM %(parents_table)s as parents + LEFT JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.from_role_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.to_role_id IS NOT NULL + + UNION + + SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.from_role_id + AND new_ancestry_list.to_id = %(ancestors_table)s.to_role_id) + WHERE %(ancestors_table)s.id IS NULL + ''' % sql_params) + insert_ct = cursor.rowcount + + if insert_ct == 0 and delete_ct == 0: + break + + role_ids_to_rebuild = Role.objects.distinct() \ + .filter(id__in=role_ids_to_rebuild, children__id__isnull=False) \ + .values_list('children__id', flat=True) + + @staticmethod def visible_roles(user): diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 2ad1250f81..3ef1bd477d 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -168,8 +168,8 @@ def test_content_object(user): assert org.admin_role.content_object.id == org.id @pytest.mark.django_db -def test_hierarchy_rebuilding(): - 'Tests some subdtle cases around role hierarchy rebuilding' +def test_hierarchy_rebuilding_multi_path(): + 'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length' X = Role.objects.create(name='X') A = Role.objects.create(name='A') @@ -196,6 +196,100 @@ def test_hierarchy_rebuilding(): assert X.is_ancestor_of(D) is False +@pytest.mark.django_db +def test_hierarchy_rebuilding_loops1(organization, team): + 'Tests ancestry rebuilding loops are involved' + + + assert team.admin_role.is_ancestor_of(organization.admin_role) is False + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.add(organization.admin_role) + + assert team.admin_role.is_ancestor_of(organization.admin_role) + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.remove(organization.admin_role) + + assert team.admin_role.is_ancestor_of(organization.admin_role) is False + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.add(organization.admin_role) + + + X = Role.objects.create(name='X') + X.children.add(organization.admin_role) + assert X.is_ancestor_of(team.admin_role) + assert X.is_ancestor_of(organization.admin_role) + assert organization.admin_role.is_ancestor_of(X) is False + assert team.admin_role.is_ancestor_of(X) is False + + #print(X.descendents.filter(id=organization.admin_role.id).count()) + #print(X.children.filter(id=organization.admin_role.id).count()) + X.children.remove(organization.admin_role) + X.rebuild_role_ancestor_list() + #print(X.descendents.filter(id=organization.admin_role.id).count()) + #print(X.children.filter(id=organization.admin_role.id).count()) + + assert X.is_ancestor_of(team.admin_role) is False + assert X.is_ancestor_of(organization.admin_role) is False + + + +@pytest.mark.django_db +def test_hierarchy_rebuilding_loops(): + 'Tests ancestry rebuilding loops are involved' + + X = Role.objects.create(name='X') + A = Role.objects.create(name='A') + B = Role.objects.create(name='B') + C = Role.objects.create(name='C') + + A.children.add(B) + B.children.add(C) + C.children.add(A) + X.children.add(A) + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + assert A.is_ancestor_of(B) + assert A.is_ancestor_of(C) + assert B.is_ancestor_of(C) + assert B.is_ancestor_of(A) + assert C.is_ancestor_of(A) + assert C.is_ancestor_of(B) + + X.children.remove(A) + X.rebuild_role_ancestor_list() + + assert X.is_ancestor_of(A) is False + assert X.is_ancestor_of(B) is False + assert X.is_ancestor_of(C) is False + + X.children.add(A) + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + C.children.remove(A) + + assert A.is_ancestor_of(B) + assert A.is_ancestor_of(C) + assert B.is_ancestor_of(C) + assert B.is_ancestor_of(A) is False + assert C.is_ancestor_of(A) is False + assert C.is_ancestor_of(B) is False + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + + + @pytest.mark.django_db def test_auto_parenting(): org1 = Organization.objects.create(name='org1') diff --git a/awx/main/tests/old/commands/commands_monolithic.py b/awx/main/tests/old/commands/commands_monolithic.py index 869b530ac9..153d1be27d 100644 --- a/awx/main/tests/old/commands/commands_monolithic.py +++ b/awx/main/tests/old/commands/commands_monolithic.py @@ -986,7 +986,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(new_inv.groups.count(), ngroups) self.assertEqual(new_inv.total_hosts, nhosts) self.assertEqual(new_inv.total_groups, ngroups) - self.assertElapsedLessThan(1200) # FIXME: This should be < 120, will drop back down next sprint during our performance tuning work - anoek 2016-03-22 + self.assertElapsedLessThan(120) @unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False), 'Skip this test in local development environments, ' diff --git a/config/awx-munin.conf b/config/awx-munin.conf index 833a6f36bf..ca4f4c39ad 100644 --- a/config/awx-munin.conf +++ b/config/awx-munin.conf @@ -9,4 +9,3 @@ Alias /munin /var/www/html/munin/ require valid-user -ScriptAlias /munin-cgi/munin-cgi-graph /var/www/cgi-bin/munin-cgi-graph \ No newline at end of file From 6d34ca9d2245bc72bb718801d743571919c02d9b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 10:03:50 -0400 Subject: [PATCH 03/56] Proof of concept hacks for RolePermission elimination --- awx/main/fields.py | 2 ++ awx/main/models/mixins.py | 26 +++++++++------------ awx/main/models/rbac.py | 1 + awx/main/signals.py | 8 +------ awx/main/tests/functional/test_rbac_core.py | 4 ++-- 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 30bd8e03f3..50d73ee4c7 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -183,6 +183,7 @@ class ImplicitRoleField(models.ForeignKey): role = Role_.objects.create( created=now(), modified=now(), + role_field=self.name, name=self.role_name, description=self.role_description ) @@ -233,6 +234,7 @@ class ImplicitRoleField(models.ForeignKey): else: role = Role_.objects.create(created=now(), modified=now(), + role_field=path, singleton_name=singleton_name, name=singleton_name, description=singleton_name) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 1396698c28..5fd7dc89f8 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -31,29 +31,25 @@ class ResourceMixin(models.Model): performant to resolve the resource in question then call `myresource.get_permissions(user)`. ''' - return ResourceMixin._accessible_objects(cls, accessor, permissions) + return ResourceMixin._accessible_objects(cls, accessor, role_name) @staticmethod - def _accessible_objects(cls, accessor, permissions): + def _accessible_objects(cls, accessor, role_name): if type(accessor) == User: - qs = cls.objects.filter( - role_permissions__role__ancestors__members=accessor - ) + kwargs = {} + kwargs[role_name + '__ancestors__members'] = accessor + qs = cls.objects.filter(**kwargs) elif type(accessor) == Role: - qs = cls.objects.filter( - role_permissions__role__ancestors=accessor - ) + kwargs = {} + kwargs[role_name + '__ancestors'] = accessor + qs = cls.objects.filter(**kwargs) else: accessor_type = ContentType.objects.get_for_model(accessor) roles = Role.objects.filter(content_type__pk=accessor_type.id, object_id=accessor.id) - qs = cls.objects.filter( - role_permissions__role__ancestors__in=roles - ) - - for perm in permissions: - qs = qs.annotate(**{'max_' + perm: Max('role_permissions__' + perm)}) - qs = qs.filter(**{'max_' + perm: int(permissions[perm])}) + kwargs = {} + kwargs[role_name + '__ancestors__in'] = roles + qs = cls.objects.filter(**kwargs) #return cls.objects.filter(resource__in=qs) return qs diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 2404831b56..86fa2b6e28 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -77,6 +77,7 @@ class Role(CommonModelNameNotUnique): db_table = 'main_rbac_roles' singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True) + role_field = models.TextField(null=False, default=None) parents = models.ManyToManyField('Role', related_name='children') implicit_parents = models.TextField(null=False, default='[]') ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list` diff --git a/awx/main/signals.py b/awx/main/signals.py index 891c60b75b..cc475b655a 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -131,16 +131,10 @@ def create_user_role(instance, **kwargs): except Role.DoesNotExist: role = Role.objects.create( name = 'Owner', + role_field='owner_role', content_object = instance, ) role.members.add(instance) - RolePermission.objects.create( - role = role, - resource = instance, - auto_generated = True, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): content_type = ContentType.objects.get_for_model(Organization) diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index a7c3275530..0be5fbd439 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -9,8 +9,8 @@ from awx.main.models import ( @pytest.mark.django_db def test_auto_inheritance_by_children(organization, alice): - A = Role.objects.create(name='A') - B = Role.objects.create(name='B') + A = Role.objects.create(name='A', role_field='') + B = Role.objects.create(name='B', role_field='') A.members.add(alice) assert alice not in organization.admin_role From 3b2c6747d3d21512ab5d35a5389a660cac4e45d1 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 10:39:51 -0400 Subject: [PATCH 04/56] Updated test_rbac_core.py test for RolePermission removal --- awx/main/tests/functional/test_rbac_core.py | 57 ++++++++++----------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 0be5fbd439..c70f221c7d 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -40,17 +40,17 @@ def test_auto_inheritance_by_parents(organization, alice): B = Role.objects.create(name='B') A.members.add(alice) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role B.parents.add(A) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role organization.admin_role.parents.add(A) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role organization.admin_role.parents.remove(A) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role organization.admin_role.parents.add(B) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role organization.admin_role.parents.remove(B) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role @@ -62,57 +62,54 @@ def test_accessible_objects(organization, alice, bob): B.members.add(alice) B.members.add(bob) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0 - RolePermission.objects.create(role=A, resource=organization, read=True) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 - RolePermission.objects.create(role=B, resource=organization, write=True) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 1 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 + assert Organization.accessible_objects(bob, 'admin_role').count() == 0 + A.children.add(organization.admin_role) + assert Organization.accessible_objects(alice, 'admin_role').count() == 1 + assert Organization.accessible_objects(bob, 'admin_role').count() == 0 @pytest.mark.django_db def test_team_symantics(organization, team, alice): - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role team.member_role.children.add(organization.auditor_role) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role team.member_role.members.add(alice) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.auditor_role team.member_role.members.remove(alice) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role @pytest.mark.django_db -def test_auto_m2m_adjuments(organization, inventory, group, alice): +def test_auto_m2m_adjustments(organization, inventory, group, alice): 'Ensures the auto role reparenting is working correctly through m2m maps' g1 = group(name='g1') g1.admin_role.members.add(alice) - assert g1.accessible_by(alice, {'read': True}) is True + assert alice in g1.admin_role g2 = group(name='g2') - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role g2.parents.add(g1) - assert g2.accessible_by(alice, {'read': True}) is True + assert alice in g2.admin_role g2.parents.remove(g1) - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role g1.children.add(g2) - assert g2.accessible_by(alice, {'read': True}) is True + assert alice in g2.admin_role g1.children.remove(g2) - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role @pytest.mark.django_db -def test_auto_field_adjuments(organization, inventory, team, alice): +def test_auto_field_adjustments(organization, inventory, team, alice): 'Ensures the auto role reparenting is working correctly through non m2m fields' org2 = Organization.objects.create(name='Org 2', description='org 2') org2.admin_role.members.add(alice) - assert inventory.accessible_by(alice, {'read': True}) is False + assert alice not in inventory.admin_role inventory.organization = org2 inventory.save() - assert inventory.accessible_by(alice, {'read': True}) is True + assert alice in inventory.admin_role inventory.organization = organization inventory.save() - assert inventory.accessible_by(alice, {'read': True}) is False + assert alice not in inventory.admin_role #assert False @pytest.mark.django_db @@ -131,14 +128,12 @@ def test_implicit_deletes(alice): assert Role.objects.filter(id=auditor_role_id).count() == 1 n_alice_roles = alice.roles.count() n_system_admin_children = Role.singleton('System Administrator').children.count() - rp = RolePermission.objects.create(role=delorg.admin_role, resource=delorg, read=True) delorg.delete() assert Role.objects.filter(id=admin_role_id).count() == 0 assert Role.objects.filter(id=auditor_role_id).count() == 0 assert alice.roles.count() == (n_alice_roles - 1) - assert RolePermission.objects.filter(id=rp.id).count() == 0 assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1) assert child.ancestors.count() == 1 assert child.ancestors.all()[0] == child From eae73496650a92fac033349bcd85635893928830 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 10:45:33 -0400 Subject: [PATCH 05/56] update access calls to new RBAC roles --- awx/main/access.py | 155 +++++++++++++++++------------------- awx/main/models/__init__.py | 1 - 2 files changed, 73 insertions(+), 83 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 3305dc8983..067158d9ee 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -23,7 +23,7 @@ from awx.main.task_engine import TaskSerializer from awx.main.conf import tower_settings __all__ = ['get_user_queryset', 'check_user_access', - 'user_accessible_objects', 'user_accessible_by', + 'user_accessible_objects', 'user_admin_role',] PERMISSION_TYPES = [ @@ -65,17 +65,8 @@ def register_access(model_class, access_class): def user_admin_role(self): return Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=self.id) -def user_accessible_objects(user, permissions): - return ResourceMixin._accessible_objects(User, user, permissions) - -def user_accessible_by(instance, user, permissions): - perms = get_user_permissions_on_resource(instance, user) - if perms is None: - return False - for k in permissions: - if k not in perms or perms[k] < permissions[k]: - return False - return True +def user_accessible_objects(user, role): + return ResourceMixin._accessible_objects(User, user, role) def get_user_queryset(user, model_class): ''' @@ -229,7 +220,7 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - return Organization.accessible_objects(self.user, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() def can_change(self, obj, data): if data is not None and 'is_superuser' in data: @@ -256,7 +247,7 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'delete': True}) + return False class OrganizationAccess(BaseAccess): @@ -272,13 +263,13 @@ class OrganizationAccess(BaseAccess): model = Organization def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by').all() def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): self.check_license(feature='multiple_organizations', check_expiration=False) @@ -307,29 +298,29 @@ class InventoryAccess(BaseAccess): model = Inventory def get_queryset(self, allowed=None, ad_hoc=None): - qs = self.model.accessible_objects(self.user, {'read': True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'organization').all() def can_read(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read': True}) + return self.user in obj.read_role def can_use(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'use': True}) + return self.user in obj.use_role 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, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() if self.user.is_superuser: return True org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}) + return self.user in org.admin_role def can_change(self, obj, data): # Verify that the user has access to the new organization if moving an @@ -337,10 +328,10 @@ class InventoryAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}): + if self.user not in org.admin_role: return False # Otherwise, just check for write permission. - return obj.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}) + return self.user in obj.admin_role def can_admin(self, obj, data): # Verify that the user has access to the new organization if moving an @@ -348,16 +339,16 @@ class InventoryAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, ALL_PERMISSIONS): + if self.user not in org.admin_role: return False # Otherwise, just check for admin permission. - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): return self.can_admin(obj, None) def can_run_ad_hoc_commands(self, obj): - return obj.accessible_by(self.user, {'execute': True}) + return self.user in adhoc_role class HostAccess(BaseAccess): ''' @@ -368,14 +359,14 @@ class HostAccess(BaseAccess): model = Host def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') qs = qs.select_related('created_by', 'modified_by', 'inventory', 'last_job__job_template', 'last_job_host_summary__job') return qs.prefetch_related('groups').all() def can_read(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'read':True}) + return obj and self.user in obj.read_role def can_add(self, data): if not data or 'inventory' not in data: @@ -384,7 +375,7 @@ class HostAccess(BaseAccess): # Checks for admin or change permission on inventory. inventory_pk = get_pk_from_dict(data, 'inventory') inventory = get_object_or_400(Inventory, pk=inventory_pk) - if not inventory.accessible_by(self.user, {'read':True, 'create':True}): + if self.user not in inventory.admin_role: return False # Check to see if we have enough licenses @@ -398,7 +389,7 @@ class HostAccess(BaseAccess): raise PermissionDenied('Unable to change inventory on a host') # Checks for admin or change permission on inventory, controls whether # the user can edit variable data. - return obj and obj.inventory.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return obj and self.user in obj.inventory.admin_role def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): @@ -411,7 +402,7 @@ class HostAccess(BaseAccess): return True def can_delete(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.inventory.admin_role class GroupAccess(BaseAccess): ''' @@ -422,12 +413,12 @@ class GroupAccess(BaseAccess): model = Group def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') qs = qs.select_related('created_by', 'modified_by', 'inventory') return qs.prefetch_related('parents', 'children', 'inventory_source').all() def can_read(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'read':True}) + return obj and self.user in obj.inventory.read_role def can_add(self, data): if not data or 'inventory' not in data: @@ -435,7 +426,7 @@ class GroupAccess(BaseAccess): # Checks for admin or change permission on inventory. inventory_pk = get_pk_from_dict(data, 'inventory') inventory = get_object_or_400(Inventory, pk=inventory_pk) - return inventory.accessible_by(self.user, {'read':True, 'create':True}) + return self.user in inventory.admin_role def can_change(self, obj, data): # Prevent moving a group to a different inventory. @@ -444,7 +435,7 @@ class GroupAccess(BaseAccess): raise PermissionDenied('Unable to change inventory on a group') # Checks for admin or change permission on inventory, controls whether # the user can attach subgroups or edit variable data. - return obj and obj.inventory.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return obj and self.user in obj.inventory.admin_role def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): @@ -465,7 +456,7 @@ class GroupAccess(BaseAccess): return True def can_delete(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.inventory.admin_role class InventorySourceAccess(BaseAccess): ''' @@ -484,9 +475,9 @@ class InventorySourceAccess(BaseAccess): def can_read(self, obj): if obj and obj.group: - return obj.group.accessible_by(self.user, {'read':True}) + return self.user in obj.group.read_role elif obj and obj.inventory: - return obj.inventory.accessible_by(self.user, {'read':True}) + return self.user in obj.inventory.read_role else: return False @@ -497,7 +488,7 @@ class InventorySourceAccess(BaseAccess): def can_change(self, obj, data): # Checks for admin or change permission on group. if obj and obj.group: - return obj.group.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return self.user in obj.group.admin_role # Can't change inventory sources attached to only the inventory, since # these are created automatically from the management command. else: @@ -547,11 +538,11 @@ class CredentialAccess(BaseAccess): """Return the queryset for credentials, based on what the user is permitted to see. """ - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by').all() def can_read(self, obj): - return obj.accessible_by(self.user, {'read': True}) + return self.user in obj.read_role def can_add(self, data): # Access enforced in our view where we have context enough to make a decision @@ -560,12 +551,12 @@ class CredentialAccess(BaseAccess): def can_use(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'use': True}) + return self.user in obj.use_role def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read':True, 'update': True, 'delete':True}) + return self.user in obj.admin_role def can_delete(self, obj): # Unassociated credentials may be marked deleted by anyone, though we @@ -588,7 +579,7 @@ class TeamAccess(BaseAccess): model = Team def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'organization').all() def can_add(self, data): @@ -597,7 +588,7 @@ class TeamAccess(BaseAccess): else: org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - if org.accessible_by(self.user, {'read':True, 'update':True, 'write':True}): + if self.user in org.admin_role: return True return False @@ -606,7 +597,7 @@ class TeamAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: raise PermissionDenied('Unable to change organization on a team') - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -632,19 +623,19 @@ class ProjectAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser: return self.model.objects.all() - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all() def can_add(self, data): if self.user.is_superuser: return True - qs = Organization.accessible_objects(self.user, ALL_PERMISSIONS) + qs = Organization.accessible_objects(self.user, 'admin_role') return qs.exists() def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -673,7 +664,7 @@ class ProjectUpdateAccess(BaseAccess): return self.can_change(obj, {}) and obj.can_cancel def can_delete(self, obj): - return obj and obj.project.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.project.admin_role class JobTemplateAccess(BaseAccess): ''' @@ -694,7 +685,7 @@ class JobTemplateAccess(BaseAccess): if self.user.is_superuser: qs = self.model.objects.all() else: - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'inventory', 'project', 'credential', 'cloud_credential', 'next_schedule').all() @@ -726,7 +717,7 @@ class JobTemplateAccess(BaseAccess): credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: credential = get_object_or_400(Credential, pk=credential_pk) - if not credential.accessible_by(self.user, {'read':True}): + if self.user not in credential.read_role: return False # If a cloud credential is provided, the user should have read access. @@ -734,7 +725,7 @@ class JobTemplateAccess(BaseAccess): if cloud_credential_pk: cloud_credential = get_object_or_400(Credential, pk=cloud_credential_pk) - if not cloud_credential.accessible_by(self.user, {'read':True}): + if self.user not in cloud_credential.read_role: return False # Check that the given inventory ID is valid. @@ -746,7 +737,7 @@ class JobTemplateAccess(BaseAccess): project_pk = get_pk_from_dict(data, 'project') if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN: org = inventory[0].organization - accessible = org.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + accessible = self.user in org.admin_role if not project_pk and accessible: return True elif not accessible: @@ -754,10 +745,10 @@ class JobTemplateAccess(BaseAccess): # If the user has admin access to the project (as an org admin), should # be able to proceed without additional checks. project = get_object_or_400(Project, pk=project_pk) - if project.accessible_by(self.user, ALL_PERMISSIONS): + if self.user in project.admin_role: return True - return project.accessible_by(self.user, ALL_PERMISSIONS) and inventory.accessible_by(self.user, {'read':True}) + return self.user in project.admin_role and self.user in inventory.read_role def can_start(self, obj, validate_license=True): # Check license. @@ -775,10 +766,10 @@ class JobTemplateAccess(BaseAccess): if obj.job_type == PERM_INVENTORY_SCAN: # Scan job with default project, must have JT execute or be org admin if obj.project is None and obj.inventory: - return (obj.accessible_by(self.user, {'execute': True}) or - obj.inventory.organization.accessible_by(self.user, ALL_PERMISSIONS)) + return (self.user in obj.execute_role or + self.user in obj.inventory.organization.admin_role) - return obj.accessible_by(self.user, {'execute':True}) + return self.user in obj.execute_role def can_change(self, obj, data): data_for_change = data @@ -813,7 +804,7 @@ class JobAccess(BaseAccess): credential_ids = self.user.get_queryset(Credential) return qs.filter( credential_id__in=credential_ids, - job_template__in=JobTemplate.accessible_objects(self.user, {'read': True}) + job_template__in=JobTemplate.accessible_objects(self.user, 'read_role') ) def can_add(self, data): @@ -846,7 +837,7 @@ class JobAccess(BaseAccess): # Allow org admins and superusers to delete jobs if self.user.is_superuser: return True - return obj.inventory.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.inventory.admin_role def can_start(self, obj): self.check_license() @@ -858,12 +849,12 @@ class JobAccess(BaseAccess): # If a user can launch the job template then they can relaunch a job from that # job template if obj.job_template is not None: - return obj.job_template.accessible_by(self.user, {'execute': True}) + return self.user in obj.job_template.execute_role - inventory_access = obj.inventory.accessible_by(self.user, {'use':True}) + inventory_access = self.user in obj.inventory.use_role - org_access = obj.inventory.organization.accessible_by(self.user, ALL_PERMISSIONS) - project_access = obj.project is None or obj.project.accessible_by(self.user, ALL_PERMISSIONS) + org_access = self.user in obj.inventory.organization.admin_role + project_access = obj.project is None or self.user in obj.project.admin_role return inventory_access and (org_access or project_access) @@ -905,7 +896,7 @@ class AdHocCommandAccess(BaseAccess): return qs.all() credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) - inventory_qs = Inventory.accessible_objects(self.user, {'read': True, 'execute': True}) + inventory_qs = Inventory.accessible_objects(self.user, 'execute_role') return qs.filter(credential_id__in=credential_ids, inventory__in=inventory_qs) @@ -920,7 +911,7 @@ class AdHocCommandAccess(BaseAccess): credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: credential = get_object_or_400(Credential, pk=credential_pk) - if not credential.accessible_by(self.user, {'read':True}): + if self.user not in credential.read_role: return False # Check that the user has the run ad hoc command permission on the @@ -928,7 +919,7 @@ class AdHocCommandAccess(BaseAccess): inventory_pk = get_pk_from_dict(data, 'inventory') if inventory_pk: inventory = get_object_or_400(Inventory, pk=inventory_pk) - if not inventory.accessible_by(self.user, {'execute': True}): + if self.user not in inventory.execute_role: return False return True @@ -1188,23 +1179,23 @@ class NotifierAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser: return qs - return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, ALL_PERMISSIONS).all()) + return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) def can_read(self, obj): if self.user.is_superuser: return True if obj.organization is not None: - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role return False def can_add(self, data): if self.user.is_superuser: return True if not data: - return Organization.accessible_objects(self.user, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in org.admin_role def can_change(self, obj, data): if self.user.is_superuser: @@ -1212,10 +1203,10 @@ class NotifierAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, ALL_PERMISSIONS): + if self.user not in org.admin_role: return False if obj.organization is not None: - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role return False def can_admin(self, obj, data): @@ -1234,7 +1225,7 @@ class NotificationAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser: return qs - return self.model.objects.filter(notifier__organization__in=Organization.accessible_objects(self.user, ALL_PERMISSIONS)) + return self.model.objects.filter(notifier__organization__in=Organization.accessible_objects(self.user, 'admin_role')) def can_read(self, obj): return self.user.can_access(Notifier, 'read', obj.notifier) @@ -1252,13 +1243,13 @@ class LabelAccess(BaseAccess): if self.user.is_superuser: return self.model.objects.all() return self.model.objects.filter( - organization__in=Organization.accessible_objects(self.user, {'read': True}) + organization__in=Organization.accessible_objects(self.user, 'read_role') ) def can_read(self, obj): if self.user.is_superuser: return True - return obj.organization.accessible_by(self.user, {'read': True}) + return self.user in obj.organization.read_role def can_add(self, data): if self.user.is_superuser: @@ -1269,7 +1260,7 @@ class LabelAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, {'read': True}) + return self.user in org.read_role def can_change(self, obj, data): if self.user.is_superuser: @@ -1278,7 +1269,7 @@ class LabelAccess(BaseAccess): if self.can_add(data) is False: return False - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -1369,12 +1360,12 @@ class CustomInventoryScriptAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser: return self.model.objects.distinct().all() - return self.model.accessible_objects(self.user, {'read':True}).all() + return self.model.accessible_objects(self.user, 'read_role').all() def can_read(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read':True}) + return self.user in obj.read_role def can_add(self, data): if self.user.is_superuser: @@ -1463,7 +1454,7 @@ class RoleAccess(BaseAccess): return True if obj.object_id and \ isinstance(obj.content_object, ResourceMixin) and \ - obj.content_object.accessible_by(self.user, {'write': True}): + self.user in obj.content_object.admin_role: return True return False diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index fef22b5e2d..b9ce674dff 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -43,7 +43,6 @@ from awx.main.access import * # noqa User.add_to_class('get_queryset', get_user_queryset) User.add_to_class('can_access', check_user_access) -User.add_to_class('accessible_by', user_accessible_by) User.add_to_class('accessible_objects', user_accessible_objects) User.add_to_class('admin_role', user_admin_role) From 859d670fc81f8bece89454f4abf16a1b9733036d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 10:59:15 -0400 Subject: [PATCH 06/56] Removed RolePermission stuff for Hosts --- awx/main/access.py | 4 +- awx/main/models/rbac.py | 2 +- awx/main/signals.py | 97 ----------------------------------------- 3 files changed, 4 insertions(+), 99 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 067158d9ee..55d43598ef 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -359,7 +359,9 @@ class HostAccess(BaseAccess): model = Host def get_queryset(self): - qs = self.model.accessible_objects(self.user, 'read_role') + inv_qs = Inventory.accessible_objects(self.user, 'read_role') + group_qs = Group.accessible_objects(self.user, 'read_role') + qs = (self.model.filter(inventory=inv_qs) | self.model.filter(group=group_qs)).distinct() qs = qs.select_related('created_by', 'modified_by', 'inventory', 'last_job__job_template', 'last_job_host_summary__job') diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 86fa2b6e28..a9898f3441 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -77,7 +77,7 @@ class Role(CommonModelNameNotUnique): db_table = 'main_rbac_roles' singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True) - role_field = models.TextField(null=False, default=None) + role_field = models.TextField(null=False, default='') parents = models.ManyToManyField('Role', related_name='children') implicit_parents = models.TextField(null=False, default='[]') ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list` diff --git a/awx/main/signals.py b/awx/main/signals.py index cc475b655a..bf0095073e 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -151,101 +151,6 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): if action == 'pre_remove': instance.content_object.admin_role.children.remove(user.admin_role) -def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, **kwargs): - 'Add/remove RolePermission entries for Group roles that contain this host' - - if action == 'post_add': - def grant(host, group): - RolePermission.objects.create( - resource=host, - role=group.admin_role, - auto_generated=True, - create=1, - read=1, write=1, - delete=1, - update=1, - execute=1, - scm_update=1, - use=1, - ) - RolePermission.objects.create( - resource=host, - role=group.auditor_role, - auto_generated=True, - read=1, - ) - RolePermission.objects.create( - resource=host, - role=group.updater_role, - auto_generated=True, - read=1, - write=1, - create=1, - use=1 - ) - RolePermission.objects.create( - resource=host, - role=group.executor_role, - auto_generated=True, - read=1, - execute=1 - ) - - if reverse: - host = instance - for group_id in pk_set: - grant(host, Group.objects.get(id=group_id)) - else: - group = instance - for host_id in pk_set: - grant(Host.objects.get(id=host_id), group) - - if action == 'pre_remove': - host_content_type = ContentType.objects.get_for_model(Host) - - def remove_grant(host, group): - RolePermission.objects.filter( - content_type = host_content_type, - object_id = host.id, - auto_generated = True, - role__in = [group.admin_role, group.updater_role, group.auditor_role, group.executor_role] - ).delete() - - if reverse: - host = instance - for group_id in pk_set: - remove_grant(host, Group.objects.get(id=group_id)) - else: - group = instance - for host_id in pk_set: - remove_grant(Host.objects.get(id=host_id), group) - - -def grant_host_access_to_inventory(instance, **kwargs): - 'Add/remove RolePermission entries for the Inventory that contains this host' - host_content_type = ContentType.objects.get_for_model(Host) - inventory_content_type = ContentType.objects.get_for_model(Inventory) - - # Clear out any existing perms.. in case we switched inventory or something - qs = RolePermission.objects.filter( - content_type=host_content_type, - object_id=instance.id, - auto_generated=True, - role__content_type=inventory_content_type - ) - if qs.count() == 1 and qs[0].role.object_id == instance.inventory.id: - # No change - return - qs.delete() - - RolePermission.objects.create( - resource=instance, - role=instance.inventory.admin_role, - auto_generated=True, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) - post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host) post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host) @@ -263,8 +168,6 @@ post_save.connect(emit_job_event_detail, sender=JobEvent) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) m2m_changed.connect(org_admin_edit_members, Role.members.through) -m2m_changed.connect(grant_host_access_to_group_roles, Group.hosts.through) -post_save.connect(grant_host_access_to_inventory, Host) post_save.connect(sync_superuser_status_to_rbac, sender=User) post_save.connect(create_user_role, sender=User) From 5bca1283b26c3fba491cddcf56bdf42abfd166e4 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 11:00:33 -0400 Subject: [PATCH 07/56] executor_role -> execute_role --- awx/api/views.py | 2 +- awx/main/migrations/0008_v300_rbac_changes.py | 8 ++++---- awx/main/migrations/_rbac.py | 8 ++++---- awx/main/models/inventory.py | 6 +++--- awx/main/models/jobs.py | 2 +- awx/main/signals.py | 4 ++-- .../south_migrations/0002_v12b2_changes.py | 2 +- .../south_migrations/0004_v12b2_changes.py | 2 +- .../south_migrations/0005_v12b2_changes.py | 2 +- .../south_migrations/0006_v12b2_changes.py | 2 +- .../south_migrations/0007_v12b2_changes.py | 2 +- awx/main/south_migrations/0008_v12changes.py | 2 +- awx/main/south_migrations/0009_v13_changes.py | 2 +- awx/main/south_migrations/0010_v13_changes.py | 2 +- awx/main/south_migrations/0012_v13_changes.py | 2 +- awx/main/south_migrations/0013_v13_changes.py | 2 +- awx/main/south_migrations/0014_v13_changes.py | 2 +- awx/main/south_migrations/0015_v14_changes.py | 2 +- awx/main/south_migrations/0016_v14_changes.py | 2 +- awx/main/south_migrations/0017_v14_changes.py | 2 +- awx/main/south_migrations/0019_v14_changes.py | 2 +- awx/main/south_migrations/0020_v14_changes.py | 2 +- awx/main/south_migrations/0021_v14_changes.py | 2 +- awx/main/south_migrations/0023_v14_changes.py | 2 +- awx/main/south_migrations/0024_v14_changes.py | 2 +- awx/main/south_migrations/0026_v14_changes.py | 2 +- ...nt_created__chg_field_jobevent_modified.py | 2 +- .../south_migrations/0034_v148_changes.py | 2 +- .../south_migrations/0035_v148_changes.py | 2 +- .../south_migrations/0037_v148_changes.py | 2 +- .../south_migrations/0038_v148_changes.py | 2 +- .../south_migrations/0039_v148_changes.py | 2 +- .../south_migrations/0043_v1411_changes.py | 2 +- .../south_migrations/0046_v200_changes.py | 2 +- .../south_migrations/0051_v200_changes.py | 2 +- .../south_migrations/0053_v210_changes.py | 2 +- .../south_migrations/0054_v210_changes.py | 2 +- .../south_migrations/0061_v210_changes.py | 2 +- .../south_migrations/0064_v220_changes.py | 2 +- .../south_migrations/0072_v240_changes.py | 2 +- .../south_migrations/0073_v240_changes.py | 2 +- awx/main/tests/functional/test_rbac_api.py | 12 +++++------ .../tests/functional/test_rbac_inventory.py | 20 +++++++++---------- .../tests/functional/test_rbac_job_start.py | 2 +- awx/main/tests/old/ad_hoc.py | 8 ++++---- 45 files changed, 71 insertions(+), 71 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index cf6849df71..348e81d19f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1354,7 +1354,7 @@ class InventoryList(ListCreateAPIView): def get_queryset(self): qs = Inventory.accessible_objects(self.request.user, {'read': True}) - qs = qs.select_related('admin_role', 'auditor_role', 'updater_role', 'executor_role') + qs = qs.select_related('admin_role', 'auditor_role', 'updater_role', 'execute_role') return qs class InventoryDetail(RetrieveUpdateDestroyAPIView): diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index ba9299ca19..f6d0afc34a 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -139,8 +139,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='group', - name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.executor_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True', permissions={b'read': True, b'execute': True}), + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( model_name='group', @@ -159,7 +159,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='inventory', - name='executor_role', + name='execute_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( @@ -184,7 +184,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='jobtemplate', - name='executor_role', + name='execute_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 32c399df81..46f9151e38 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -232,7 +232,7 @@ def migrate_inventory(apps, schema_editor): raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type))) if perm.run_ad_hoc_commands: - execrole = inventory.executor_role + execrole = inventory.execute_role if perm.team: if role: @@ -392,12 +392,12 @@ def migrate_job_templates(apps, schema_editor): for team in Team.objects.iterator(): if permission.filter(team=team).exists(): - team.member_role.children.add(jt.executor_role) + team.member_role.children.add(jt.execute_role) logger.info(smart_text(u'adding Team({}) access to JobTemplate({})'.format(team.name, jt.name))) for user in User.objects.iterator(): if permission.filter(user=user).exists(): - jt.executor_role.members.add(user) + jt.execute_role.members.add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) if jt.accessible_by(user, {'execute': True}): @@ -407,5 +407,5 @@ def migrate_job_templates(apps, schema_editor): continue if old_access.check_user_access(user, jt.__class__, 'start', jt, False): - jt.executor_role.members.add(user) + jt.execute_role.members.add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 90515c2895..8348575564 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -114,7 +114,7 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', ) - executor_role = ImplicitRoleField( + execute_role = ImplicitRoleField( role_name='Inventory Executor', role_description='May execute jobs against this inventory', ) @@ -529,9 +529,9 @@ class Group(CommonModelNameNotUnique, ResourceMixin): role_name='Inventory Group Updater', parent_role=['inventory.updater_role', 'parents.updater_role'], ) - executor_role = ImplicitRoleField( + execute_role = ImplicitRoleField( role_name='Inventory Group Executor', - parent_role=['inventory.executor_role', 'parents.executor_role'], + parent_role=['inventory.execute_role', 'parents.executor_role'], ) def __unicode__(self): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index d9747f71cf..0cc3cc73af 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -213,7 +213,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): role_description='Read-only access to all settings', parent_role='project.auditor_role', ) - executor_role = ImplicitRoleField( + execute_role = ImplicitRoleField( role_name='Job Template Runner', role_description='May run the job template', ) diff --git a/awx/main/signals.py b/awx/main/signals.py index cc475b655a..70f59a8c97 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -185,7 +185,7 @@ def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, * ) RolePermission.objects.create( resource=host, - role=group.executor_role, + role=group.execute_role, auto_generated=True, read=1, execute=1 @@ -208,7 +208,7 @@ def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, * content_type = host_content_type, object_id = host.id, auto_generated = True, - role__in = [group.admin_role, group.updater_role, group.auditor_role, group.executor_role] + role__in = [group.admin_role, group.updater_role, group.auditor_role, group.execute_role] ).delete() if reverse: diff --git a/awx/main/south_migrations/0002_v12b2_changes.py b/awx/main/south_migrations/0002_v12b2_changes.py index 06fd7bd219..da4992b514 100644 --- a/awx/main/south_migrations/0002_v12b2_changes.py +++ b/awx/main/south_migrations/0002_v12b2_changes.py @@ -661,4 +661,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0004_v12b2_changes.py b/awx/main/south_migrations/0004_v12b2_changes.py index f8984566dc..796f3a3314 100644 --- a/awx/main/south_migrations/0004_v12b2_changes.py +++ b/awx/main/south_migrations/0004_v12b2_changes.py @@ -340,4 +340,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0005_v12b2_changes.py b/awx/main/south_migrations/0005_v12b2_changes.py index 7b136aa916..6a451eec97 100644 --- a/awx/main/south_migrations/0005_v12b2_changes.py +++ b/awx/main/south_migrations/0005_v12b2_changes.py @@ -270,4 +270,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0006_v12b2_changes.py b/awx/main/south_migrations/0006_v12b2_changes.py index 8193359d54..9440d54b72 100644 --- a/awx/main/south_migrations/0006_v12b2_changes.py +++ b/awx/main/south_migrations/0006_v12b2_changes.py @@ -255,4 +255,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0007_v12b2_changes.py b/awx/main/south_migrations/0007_v12b2_changes.py index 1a08e77746..2894db993a 100644 --- a/awx/main/south_migrations/0007_v12b2_changes.py +++ b/awx/main/south_migrations/0007_v12b2_changes.py @@ -268,4 +268,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0008_v12changes.py b/awx/main/south_migrations/0008_v12changes.py index c8be2a290e..ea98d42f3e 100644 --- a/awx/main/south_migrations/0008_v12changes.py +++ b/awx/main/south_migrations/0008_v12changes.py @@ -257,4 +257,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0009_v13_changes.py b/awx/main/south_migrations/0009_v13_changes.py index 9fdf67f616..51db04032b 100644 --- a/awx/main/south_migrations/0009_v13_changes.py +++ b/awx/main/south_migrations/0009_v13_changes.py @@ -405,4 +405,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0010_v13_changes.py b/awx/main/south_migrations/0010_v13_changes.py index f57569f820..239852d336 100644 --- a/awx/main/south_migrations/0010_v13_changes.py +++ b/awx/main/south_migrations/0010_v13_changes.py @@ -518,4 +518,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0012_v13_changes.py b/awx/main/south_migrations/0012_v13_changes.py index eb3bf704a4..1826d17d8e 100644 --- a/awx/main/south_migrations/0012_v13_changes.py +++ b/awx/main/south_migrations/0012_v13_changes.py @@ -327,4 +327,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0013_v13_changes.py b/awx/main/south_migrations/0013_v13_changes.py index 723416eba6..0fe5bddf26 100644 --- a/awx/main/south_migrations/0013_v13_changes.py +++ b/awx/main/south_migrations/0013_v13_changes.py @@ -331,4 +331,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0014_v13_changes.py b/awx/main/south_migrations/0014_v13_changes.py index f706a6938d..4416539998 100644 --- a/awx/main/south_migrations/0014_v13_changes.py +++ b/awx/main/south_migrations/0014_v13_changes.py @@ -336,4 +336,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0015_v14_changes.py b/awx/main/south_migrations/0015_v14_changes.py index 042a79a3a9..bcb8dd06ea 100644 --- a/awx/main/south_migrations/0015_v14_changes.py +++ b/awx/main/south_migrations/0015_v14_changes.py @@ -335,4 +335,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0016_v14_changes.py b/awx/main/south_migrations/0016_v14_changes.py index b480df3a30..eae2f42094 100644 --- a/awx/main/south_migrations/0016_v14_changes.py +++ b/awx/main/south_migrations/0016_v14_changes.py @@ -427,4 +427,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0017_v14_changes.py b/awx/main/south_migrations/0017_v14_changes.py index 3593f369fa..af1c4821e4 100644 --- a/awx/main/south_migrations/0017_v14_changes.py +++ b/awx/main/south_migrations/0017_v14_changes.py @@ -388,4 +388,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0019_v14_changes.py b/awx/main/south_migrations/0019_v14_changes.py index a541e998d0..20932fbf8f 100644 --- a/awx/main/south_migrations/0019_v14_changes.py +++ b/awx/main/south_migrations/0019_v14_changes.py @@ -497,4 +497,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0020_v14_changes.py b/awx/main/south_migrations/0020_v14_changes.py index d561dc8d1a..829870b256 100644 --- a/awx/main/south_migrations/0020_v14_changes.py +++ b/awx/main/south_migrations/0020_v14_changes.py @@ -451,4 +451,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0021_v14_changes.py b/awx/main/south_migrations/0021_v14_changes.py index e8678f652a..1d2b4c02e4 100644 --- a/awx/main/south_migrations/0021_v14_changes.py +++ b/awx/main/south_migrations/0021_v14_changes.py @@ -501,4 +501,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0023_v14_changes.py b/awx/main/south_migrations/0023_v14_changes.py index 0173c0f9c6..cf0425adff 100644 --- a/awx/main/south_migrations/0023_v14_changes.py +++ b/awx/main/south_migrations/0023_v14_changes.py @@ -464,4 +464,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0024_v14_changes.py b/awx/main/south_migrations/0024_v14_changes.py index 25d5a58fb1..596dbb5f52 100644 --- a/awx/main/south_migrations/0024_v14_changes.py +++ b/awx/main/south_migrations/0024_v14_changes.py @@ -395,4 +395,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0026_v14_changes.py b/awx/main/south_migrations/0026_v14_changes.py index 54fdad9e30..cbf6351ec0 100644 --- a/awx/main/south_migrations/0026_v14_changes.py +++ b/awx/main/south_migrations/0026_v14_changes.py @@ -417,4 +417,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py b/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py index ba67bd835f..a963531fd0 100644 --- a/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py +++ b/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py @@ -427,4 +427,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0034_v148_changes.py b/awx/main/south_migrations/0034_v148_changes.py index be0d8f88ed..8a6e982c4d 100644 --- a/awx/main/south_migrations/0034_v148_changes.py +++ b/awx/main/south_migrations/0034_v148_changes.py @@ -442,4 +442,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0035_v148_changes.py b/awx/main/south_migrations/0035_v148_changes.py index 354b7b166f..0b0fd4bb04 100644 --- a/awx/main/south_migrations/0035_v148_changes.py +++ b/awx/main/south_migrations/0035_v148_changes.py @@ -1408,4 +1408,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0037_v148_changes.py b/awx/main/south_migrations/0037_v148_changes.py index 587f471430..36fff3cb02 100644 --- a/awx/main/south_migrations/0037_v148_changes.py +++ b/awx/main/south_migrations/0037_v148_changes.py @@ -792,4 +792,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0038_v148_changes.py b/awx/main/south_migrations/0038_v148_changes.py index 4c1d877d7e..d172989417 100644 --- a/awx/main/south_migrations/0038_v148_changes.py +++ b/awx/main/south_migrations/0038_v148_changes.py @@ -497,4 +497,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0039_v148_changes.py b/awx/main/south_migrations/0039_v148_changes.py index 2fdaf3800a..977f69df69 100644 --- a/awx/main/south_migrations/0039_v148_changes.py +++ b/awx/main/south_migrations/0039_v148_changes.py @@ -449,4 +449,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0043_v1411_changes.py b/awx/main/south_migrations/0043_v1411_changes.py index 8abe29675a..1233cb28b2 100644 --- a/awx/main/south_migrations/0043_v1411_changes.py +++ b/awx/main/south_migrations/0043_v1411_changes.py @@ -436,4 +436,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0046_v200_changes.py b/awx/main/south_migrations/0046_v200_changes.py index 17b38fed54..71272ae571 100644 --- a/awx/main/south_migrations/0046_v200_changes.py +++ b/awx/main/south_migrations/0046_v200_changes.py @@ -437,4 +437,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0051_v200_changes.py b/awx/main/south_migrations/0051_v200_changes.py index 268e075769..5d948183fe 100644 --- a/awx/main/south_migrations/0051_v200_changes.py +++ b/awx/main/south_migrations/0051_v200_changes.py @@ -440,4 +440,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0053_v210_changes.py b/awx/main/south_migrations/0053_v210_changes.py index 43b6c52de9..6bcdc9e609 100644 --- a/awx/main/south_migrations/0053_v210_changes.py +++ b/awx/main/south_migrations/0053_v210_changes.py @@ -450,4 +450,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0054_v210_changes.py b/awx/main/south_migrations/0054_v210_changes.py index 762515e3c5..84a63c8222 100644 --- a/awx/main/south_migrations/0054_v210_changes.py +++ b/awx/main/south_migrations/0054_v210_changes.py @@ -493,4 +493,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0061_v210_changes.py b/awx/main/south_migrations/0061_v210_changes.py index 26573c9e05..2553a29906 100644 --- a/awx/main/south_migrations/0061_v210_changes.py +++ b/awx/main/south_migrations/0061_v210_changes.py @@ -510,4 +510,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0064_v220_changes.py b/awx/main/south_migrations/0064_v220_changes.py index a58c74ac6b..a94f867266 100644 --- a/awx/main/south_migrations/0064_v220_changes.py +++ b/awx/main/south_migrations/0064_v220_changes.py @@ -572,4 +572,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0072_v240_changes.py b/awx/main/south_migrations/0072_v240_changes.py index d6d30695d5..721b453087 100644 --- a/awx/main/south_migrations/0072_v240_changes.py +++ b/awx/main/south_migrations/0072_v240_changes.py @@ -519,4 +519,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0073_v240_changes.py b/awx/main/south_migrations/0073_v240_changes.py index 3c844bc09b..743aaee279 100644 --- a/awx/main/south_migrations/0073_v240_changes.py +++ b/awx/main/south_migrations/0073_v240_changes.py @@ -520,4 +520,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index 6200a46289..b2dd1af74d 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -275,7 +275,7 @@ def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplat assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False - res =post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, org_admin) + res =post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, org_admin) print(res.data) assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True @@ -287,12 +287,12 @@ def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemp org_admin = user('org-admin') joe = user('joe') organization.admin_role.members.add(org_admin) - check_jobtemplate.executor_role.members.add(joe) + check_jobtemplate.execute_role.members.add(joe) assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) + post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False @@ -305,7 +305,7 @@ def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemp assert check_jobtemplate.accessible_by(rando, {'write': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False - res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, rando) + res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, rando) print(res.data) assert res.status_code == 403 @@ -317,12 +317,12 @@ def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobt 'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so' rando = user('rando') joe = user('joe') - check_jobtemplate.executor_role.members.add(joe) + check_jobtemplate.execute_role.members.add(joe) assert check_jobtemplate.accessible_by(rando, {'write': True}) is False assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, rando) + res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, rando) assert res.status_code == 403 assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index b2196eb626..33d7bf8580 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -32,7 +32,7 @@ def test_inventory_admin_user(inventory, permissions, user): rbac.migrate_inventory(apps, None) assert inventory.accessible_by(u, permissions['admin']) - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db @@ -48,7 +48,7 @@ def test_inventory_auditor_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['auditor']) is True - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db @@ -63,7 +63,7 @@ def test_inventory_updater_user(inventory, permissions, user): rbac.migrate_inventory(apps, None) assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() @pytest.mark.django_db @@ -79,7 +79,7 @@ def test_inventory_executor_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['auditor']) is True - assert inventory.executor_role.members.filter(id=u.id).exists() + assert inventory.execute_role.members.filter(id=u.id).exists() assert inventory.updater_role.members.filter(id=u.id).exists() is False @@ -99,7 +99,7 @@ def test_inventory_admin_team(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False assert inventory.accessible_by(u, permissions['auditor']) assert inventory.accessible_by(u, permissions['admin']) @@ -121,7 +121,7 @@ def test_inventory_auditor(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False assert inventory.accessible_by(u, permissions['auditor']) assert inventory.accessible_by(u, permissions['admin']) is False @@ -142,10 +142,10 @@ def test_inventory_updater(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False assert team.member_role.is_ancestor_of(inventory.updater_role) - assert team.member_role.is_ancestor_of(inventory.executor_role) is False + assert team.member_role.is_ancestor_of(inventory.execute_role) is False @pytest.mark.django_db @@ -164,10 +164,10 @@ def test_inventory_executor(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.updater_role.members.filter(id=u.id).exists() is False assert team.member_role.is_ancestor_of(inventory.updater_role) is False - assert team.member_role.is_ancestor_of(inventory.executor_role) + assert team.member_role.is_ancestor_of(inventory.execute_role) @pytest.mark.django_db def test_group_parent_admin(group, permissions, user): diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index 67e661431d..0bbdc3242e 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -24,7 +24,7 @@ def test_admin_executing_permissions(deploy_jobtemplate, inventory, machine_cred def test_job_template_start_access(deploy_jobtemplate, user): common_user = user('test-user', False) - deploy_jobtemplate.executor_role.members.add(common_user) + deploy_jobtemplate.execute_role.members.add(common_user) assert common_user.can_access(JobTemplate, 'start', deploy_jobtemplate) diff --git a/awx/main/tests/old/ad_hoc.py b/awx/main/tests/old/ad_hoc.py index 3b3f24391e..d01093c0b4 100644 --- a/awx/main/tests/old/ad_hoc.py +++ b/awx/main/tests/old/ad_hoc.py @@ -471,7 +471,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Add executor role permissions to other. Fails # when other user can't read credential. with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) with self.current_user('other'): self.run_test_ad_hoc_command(expect=403) @@ -504,7 +504,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Give the nobody user the run_ad_hoc_commands flag, and can now see # the one ad hoc command previously run. with self.current_user('admin'): - response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) qs = AdHocCommand.objects.filter(credential_id=nobody_cred.pk) self.assertEqual(qs.count(), 1) self.check_get_list(url, 'nobody', qs) @@ -1006,7 +1006,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # can_run_ad_hoc_commands = True when we shouldn't. nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,)) with self.current_user('admin'): - response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) # Create a credential for the other user and explicitly give other # user admin permission on the inventory (still not allowed to run ad @@ -1025,7 +1025,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Update permission to allow other user to run ad hoc commands. Can # only see his own ad hoc commands (because of credential permission). with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) with self.current_user('other'): response = self.get(url, expect=200) self.assertEqual(response['count'], 0) From 7098ef8da5120b3bcd7874d1e42c571c0587a07d Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 11:01:08 -0400 Subject: [PATCH 08/56] usage_role -> use_role --- awx/main/migrations/0008_v300_rbac_changes.py | 4 ++-- awx/main/migrations/_rbac.py | 6 ++--- awx/main/models/credential.py | 2 +- awx/main/models/inventory.py | 2 +- .../tests/functional/test_rbac_credential.py | 6 ++--- .../tests/functional/test_rbac_job_start.py | 4 ++-- awx/main/tests/job_base.py | 22 +++++++++---------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index f6d0afc34a..b35b860997 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -109,7 +109,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='credential', - name='usage_role', + name='use_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True', permissions={b'use': True}), ), migrations.AddField( @@ -169,7 +169,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='inventory', - name='usage_role', + name='use_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True', permissions={b'use': True}), ), migrations.AddField( diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 46f9151e38..9dd22baba4 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -113,7 +113,7 @@ def attrfunc(attr_path): def _update_credential_parents(org, cred): org.admin_role.children.add(cred.owner_role) - org.member_role.children.add(cred.usage_role) + org.member_role.children.add(cred.use_role) cred.deprecated_user, cred.deprecated_team = None, None cred.save() @@ -147,7 +147,7 @@ def _discover_credentials(instances, cred, orgfunc): # Unlink the old information from the new credential cred.deprecated_user, cred.deprecated_team = None, None - cred.owner_role, cred.usage_role = None, None + cred.owner_role, cred.use_role = None, None cred.save() for i in orgs[org]: @@ -189,7 +189,7 @@ def migrate_credential(apps, schema_editor): if cred.deprecated_team is not None: cred.deprecated_team.admin_role.children.add(cred.owner_role) - cred.deprecated_team.member_role.children.add(cred.usage_role) + cred.deprecated_team.member_role.children.add(cred.use_role) cred.deprecated_user, cred.deprecated_team = None, None cred.save() logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host))) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 1b20476c58..3d09cf462e 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -182,7 +182,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], ) - usage_role = ImplicitRoleField( + use_role = ImplicitRoleField( role_name='Credential User', role_description='May use this credential, but not read sensitive portions or modify it', ) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 8348575564..846b735e1b 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -110,7 +110,7 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory Updater', role_description='May update the inventory', ) - usage_role = ImplicitRoleField( + use_role = ImplicitRoleField( role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', ) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 39a4767ad6..61653a0fae 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -19,9 +19,9 @@ def test_credential_migration_user(credential, user, permissions): assert credential.accessible_by(u, permissions['admin']) @pytest.mark.django_db -def test_credential_usage_role(credential, user, permissions): +def test_credential_use_role(credential, user, permissions): u = user('user', False) - credential.usage_role.members.add(u) + credential.use_role.members.add(u) assert credential.accessible_by(u, permissions['usage']) @pytest.mark.django_db @@ -34,7 +34,7 @@ def test_credential_migration_team_member(credential, team, user, permissions): # No permissions pre-migration (this happens automatically so we patch this) team.admin_role.children.remove(credential.owner_role) - team.member_role.children.remove(credential.usage_role) + team.member_role.children.remove(credential.use_role) assert not credential.accessible_by(u, permissions['admin']) rbac.migrate_credential(apps, None) diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index 0bbdc3242e..18060126e1 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -33,7 +33,7 @@ def test_job_template_start_access(deploy_jobtemplate, user): def test_credential_use_access(machine_credential, user): common_user = user('test-user', False) - machine_credential.usage_role.members.add(common_user) + machine_credential.use_role.members.add(common_user) assert common_user.can_access(Credential, 'use', machine_credential) @@ -42,6 +42,6 @@ def test_credential_use_access(machine_credential, user): def test_inventory_use_access(inventory, user): common_user = user('test-user', False) - inventory.usage_role.members.add(common_user) + inventory.use_role.members.add(common_user) assert common_user.can_access(Inventory, 'use', inventory) diff --git a/awx/main/tests/job_base.py b/awx/main/tests/job_base.py index 9cea21e2cd..f3a579f8c5 100644 --- a/awx/main/tests/job_base.py +++ b/awx/main/tests/job_base.py @@ -295,14 +295,14 @@ class BaseJobTestMixin(BaseTestMixin): password='ASK', created_by=self.user_sue, ) - self.cred_bob.usage_role.members.add(self.user_bob) + self.cred_bob.use_role.members.add(self.user_bob) self.cred_chuck = Credential.objects.create( username='chuck', ssh_key_data=TEST_SSH_KEY_DATA, created_by=self.user_sue, ) - self.cred_chuck.usage_role.members.add(self.user_chuck) + self.cred_chuck.use_role.members.add(self.user_chuck) self.cred_doug = Credential.objects.create( username='doug', @@ -310,7 +310,7 @@ class BaseJobTestMixin(BaseTestMixin): 'is why we dont\'t let doug actually run jobs.', created_by=self.user_sue, ) - self.cred_doug.usage_role.members.add(self.user_doug) + self.cred_doug.use_role.members.add(self.user_doug) self.cred_eve = Credential.objects.create( username='eve', @@ -320,14 +320,14 @@ class BaseJobTestMixin(BaseTestMixin): become_password='ASK', created_by=self.user_sue, ) - self.cred_eve.usage_role.members.add(self.user_eve) + self.cred_eve.use_role.members.add(self.user_eve) self.cred_frank = Credential.objects.create( username='frank', password='fr@nk the t@nk', created_by=self.user_sue, ) - self.cred_frank.usage_role.members.add(self.user_frank) + self.cred_frank.use_role.members.add(self.user_frank) self.cred_greg = Credential.objects.create( username='greg', @@ -335,21 +335,21 @@ class BaseJobTestMixin(BaseTestMixin): ssh_key_unlock='ASK', created_by=self.user_sue, ) - self.cred_greg.usage_role.members.add(self.user_greg) + self.cred_greg.use_role.members.add(self.user_greg) self.cred_holly = Credential.objects.create( username='holly', password='holly rocks', created_by=self.user_sue, ) - self.cred_holly.usage_role.members.add(self.user_holly) + self.cred_holly.use_role.members.add(self.user_holly) self.cred_iris = Credential.objects.create( username='iris', password='ASK', created_by=self.user_sue, ) - self.cred_iris.usage_role.members.add(self.user_iris) + self.cred_iris.use_role.members.add(self.user_iris) # Each operations team also has shared credentials they can use. self.cred_ops_east = Credential.objects.create( @@ -358,14 +358,14 @@ class BaseJobTestMixin(BaseTestMixin): ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK, created_by = self.user_sue, ) - self.team_ops_east.member_role.children.add(self.cred_ops_east.usage_role) + self.team_ops_east.member_role.children.add(self.cred_ops_east.use_role) self.cred_ops_west = Credential.objects.create( username='west', password='Heading270', created_by = self.user_sue, ) - self.team_ops_west.member_role.children.add(self.cred_ops_west.usage_role) + self.team_ops_west.member_role.children.add(self.cred_ops_west.use_role) # FIXME: This code can be removed (probably) @@ -391,7 +391,7 @@ class BaseJobTestMixin(BaseTestMixin): password='HeadingNone', created_by = self.user_sue, ) - self.team_ops_testers.member_role.children.add(self.cred_ops_test.usage_role) + self.team_ops_testers.member_role.children.add(self.cred_ops_test.use_role) self.ops_east_permission = Permission.objects.create( inventory = self.inv_ops_east, From d50825474287c8b3d6687916a130c15ad35195bb Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 11:01:35 -0400 Subject: [PATCH 09/56] updater_role -> update_role --- awx/api/views.py | 2 +- awx/main/migrations/0008_v300_rbac_changes.py | 6 +++--- awx/main/migrations/_rbac.py | 2 +- awx/main/models/inventory.py | 6 +++--- awx/main/signals.py | 4 ++-- .../tests/functional/test_rbac_inventory.py | 20 +++++++++---------- awx/main/tests/old/ad_hoc.py | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 348e81d19f..9831ecac03 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1354,7 +1354,7 @@ class InventoryList(ListCreateAPIView): def get_queryset(self): qs = Inventory.accessible_objects(self.request.user, {'read': True}) - qs = qs.select_related('admin_role', 'auditor_role', 'updater_role', 'execute_role') + qs = qs.select_related('admin_role', 'auditor_role', 'update_role', 'execute_role') return qs class InventoryDetail(RetrieveUpdateDestroyAPIView): diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index b35b860997..c912665970 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -144,8 +144,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='group', - name='updater_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.updater_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True', permissions={b'read': True, b'write': True, b'create': True, b'use': True}), + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True', permissions={b'read': True, b'write': True, b'create': True, b'use': True}), ), migrations.AddField( model_name='inventory', @@ -164,7 +164,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='inventory', - name='updater_role', + name='update_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True', permissions={b'read': True, b'update': True}), ), migrations.AddField( diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 9dd22baba4..04009a7453 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -214,7 +214,7 @@ def migrate_inventory(apps, schema_editor): elif perm.permission_type == 'read': return inventory.auditor_role elif perm.permission_type == 'write': - return inventory.updater_role + return inventory.update_role elif perm.permission_type == 'check' or perm.permission_type == 'run': # These permission types are handled differntly in RBAC now, nothing to migrate. return False diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 846b735e1b..cad8395ca8 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -106,7 +106,7 @@ class Inventory(CommonModel, ResourceMixin): role_description='May view but not modify this inventory', parent_role='organization.auditor_role', ) - updater_role = ImplicitRoleField( + update_role = ImplicitRoleField( role_name='Inventory Updater', role_description='May update the inventory', ) @@ -525,9 +525,9 @@ class Group(CommonModelNameNotUnique, ResourceMixin): role_name='Inventory Group Auditor', parent_role=['inventory.auditor_role', 'parents.auditor_role'], ) - updater_role = ImplicitRoleField( + update_role = ImplicitRoleField( role_name='Inventory Group Updater', - parent_role=['inventory.updater_role', 'parents.updater_role'], + parent_role=['inventory.update_role', 'parents.updater_role'], ) execute_role = ImplicitRoleField( role_name='Inventory Group Executor', diff --git a/awx/main/signals.py b/awx/main/signals.py index 70f59a8c97..f2e4abef90 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -176,7 +176,7 @@ def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, * ) RolePermission.objects.create( resource=host, - role=group.updater_role, + role=group.update_role, auto_generated=True, read=1, write=1, @@ -208,7 +208,7 @@ def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, * content_type = host_content_type, object_id = host.id, auto_generated = True, - role__in = [group.admin_role, group.updater_role, group.auditor_role, group.execute_role] + role__in = [group.admin_role, group.update_role, group.auditor_role, group.execute_role] ).delete() if reverse: diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 33d7bf8580..4434e199a0 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -33,7 +33,7 @@ def test_inventory_admin_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db def test_inventory_auditor_user(inventory, permissions, user): @@ -49,7 +49,7 @@ def test_inventory_auditor_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['auditor']) is True assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db def test_inventory_updater_user(inventory, permissions, user): @@ -64,7 +64,7 @@ def test_inventory_updater_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() + assert inventory.update_role.members.filter(id=u.id).exists() @pytest.mark.django_db def test_inventory_executor_user(inventory, permissions, user): @@ -80,7 +80,7 @@ def test_inventory_executor_user(inventory, permissions, user): assert inventory.accessible_by(u, permissions['admin']) is False assert inventory.accessible_by(u, permissions['auditor']) is True assert inventory.execute_role.members.filter(id=u.id).exists() - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False @@ -100,7 +100,7 @@ def test_inventory_admin_team(inventory, permissions, user, team): assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False assert inventory.accessible_by(u, permissions['auditor']) assert inventory.accessible_by(u, permissions['admin']) @@ -122,7 +122,7 @@ def test_inventory_auditor(inventory, permissions, user, team): assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False assert inventory.accessible_by(u, permissions['auditor']) assert inventory.accessible_by(u, permissions['admin']) is False @@ -143,8 +143,8 @@ def test_inventory_updater(inventory, permissions, user, team): assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert team.member_role.is_ancestor_of(inventory.updater_role) + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert team.member_role.is_ancestor_of(inventory.update_role) assert team.member_role.is_ancestor_of(inventory.execute_role) is False @@ -165,8 +165,8 @@ def test_inventory_executor(inventory, permissions, user, team): assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert team.member_role.is_ancestor_of(inventory.updater_role) is False + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert team.member_role.is_ancestor_of(inventory.update_role) is False assert team.member_role.is_ancestor_of(inventory.execute_role) @pytest.mark.django_db diff --git a/awx/main/tests/old/ad_hoc.py b/awx/main/tests/old/ad_hoc.py index d01093c0b4..f69d58072d 100644 --- a/awx/main/tests/old/ad_hoc.py +++ b/awx/main/tests/old/ad_hoc.py @@ -463,7 +463,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # not allowed to run ad hoc commands). user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,)) with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204) with self.current_user('other'): self.run_test_ad_hoc_command(expect=403) self.check_get_list(url, 'other', qs) @@ -1014,7 +1014,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): other_cred = self.create_test_credential(user=self.other_django_user) user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,)) with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204) with self.current_user('other'): response = self.get(url, expect=200) self.assertEqual(response['count'], 0) From ff3be050fa374651d43db87b8b6418dfdbfc134e Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 11:56:08 -0400 Subject: [PATCH 10/56] test fixes and read_role --- awx/main/models/organization.py | 5 ++ awx/main/models/projects.py | 7 +++ awx/main/tests/functional/test_rbac_api.py | 30 +++++----- .../tests/functional/test_rbac_credential.py | 24 ++++---- .../functional/test_rbac_job_templates.py | 56 +++++++++---------- .../functional/test_rbac_organization.py | 8 +-- .../tests/functional/test_rbac_project.py | 32 +++++------ awx/main/tests/functional/test_rbac_team.py | 6 +- awx/main/tests/functional/test_rbac_user.py | 10 ++-- 9 files changed, 93 insertions(+), 85 deletions(-) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index f497d2a120..7bbe1ed79f 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -67,6 +67,11 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): role_description='A member of this organization', parent_role='admin_role', ) + read_role = ImplicitRoleField( + role_name='Organization Read Access', + role_description='Read an organization', + parent_role='member_role', + ) def get_absolute_url(self): diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 0f023ab56b..097a714fdf 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -239,7 +239,14 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): member_role = ImplicitRoleField( role_name='Project Member', role_description='Implies membership within this project', + parent_role='admin_role', ) + read_role = ImplicitRoleField( + role_name='Project Read Access', + role_description='Read access to this project', + parent_role='member_role', + ) + scm_update_role = ImplicitRoleField( role_name='Project Updater', role_description='May update this project from the source control management system', diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index b2dd1af74d..9a3257875c 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -272,13 +272,11 @@ def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplat joe = user('joe') organization.admin_role.members.add(org_admin) - assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert org_admin in check_jobtemplate.admin_role + assert joe not in check_jobtemplate.execute_role - res =post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, org_admin) - - print(res.data) - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, org_admin) + assert joe in check_jobtemplate.execute_role @pytest.mark.django_db(transaction=True) @@ -289,12 +287,12 @@ def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemp organization.admin_role.members.add(org_admin) check_jobtemplate.execute_role.members.add(joe) - assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert org_admin in check_jobtemplate.admin_role + assert joe in check_jobtemplate.execute_role post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) + assert joe not in check_jobtemplate.execute - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False @pytest.mark.django_db(transaction=True) def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemplate, user): @@ -302,14 +300,13 @@ def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemp rando = user('rando') joe = user('joe') - assert check_jobtemplate.accessible_by(rando, {'write': True}) is False - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert rando not in check_jobtemplate.admin_role + assert joe not in check_jobtemplate.execute_role res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, rando) - print(res.data) assert res.status_code == 403 - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe not in check_jobtemplate.execute_role @pytest.mark.django_db(transaction=True) @@ -319,14 +316,13 @@ def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobt joe = user('joe') check_jobtemplate.execute_role.members.add(joe) - assert check_jobtemplate.accessible_by(rando, {'write': True}) is False - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert rando not in check_jobtemplate.admin_role + assert joe not in check_jobtemplate.execute_role res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, rando) assert res.status_code == 403 - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - + assert joe in check_jobtemplate.execute_role # # /roles//teams/ diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 61653a0fae..1bc584c5c9 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -16,13 +16,13 @@ def test_credential_migration_user(credential, user, permissions): rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, permissions['admin']) + assert u in credential.owner_role @pytest.mark.django_db def test_credential_use_role(credential, user, permissions): u = user('user', False) credential.use_role.members.add(u) - assert credential.accessible_by(u, permissions['usage']) + assert u in credential.owner_role @pytest.mark.django_db def test_credential_migration_team_member(credential, team, user, permissions): @@ -35,12 +35,12 @@ def test_credential_migration_team_member(credential, team, user, permissions): # No permissions pre-migration (this happens automatically so we patch this) team.admin_role.children.remove(credential.owner_role) team.member_role.children.remove(credential.use_role) - assert not credential.accessible_by(u, permissions['admin']) + assert u not in credential.owner_role rbac.migrate_credential(apps, None) # Admin permissions post migration - assert credential.accessible_by(u, permissions['admin']) + assert u in credential.owner_role @pytest.mark.django_db def test_credential_migration_team_admin(credential, team, user, permissions): @@ -49,11 +49,11 @@ def test_credential_migration_team_admin(credential, team, user, permissions): credential.deprecated_team = team credential.save() - assert not credential.accessible_by(u, permissions['usage']) + assert u not in credential.use_role # Usage permissions post migration rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, permissions['usage']) + assert u in credential.use_role def test_credential_access_superuser(): u = User(username='admin', is_superuser=True) @@ -166,10 +166,10 @@ def test_cred_inventory_source(user, inventory, credential): inventory=inventory, ) - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role @pytest.mark.django_db def test_cred_project(user, credential, project): @@ -178,10 +178,10 @@ def test_cred_project(user, credential, project): project.credential = credential project.save() - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role @pytest.mark.django_db def test_cred_no_org(user, credential): @@ -196,7 +196,7 @@ def test_cred_team(user, team, credential): credential.deprecated_team = team credential.save() - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 7cf083da2e..93538f67f0 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -27,16 +27,16 @@ def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, use rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert check_jobtemplate.project.accessible_by(joe, {'read': True}) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in check_jobtemplate.project.read_role + assert admin in check_jobtemplate.execute_role + assert joe not in check_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role @pytest.mark.django_db def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, user): @@ -55,16 +55,16 @@ def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, us rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert deploy_jobtemplate.project.accessible_by(joe, {'read': True}) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in deploy_jobtemplate.project.read_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in deploy_jobtemplate.execute_role + assert joe in deploy_jobtemplate.execute_role + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role @pytest.mark.django_db @@ -87,17 +87,17 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert check_jobtemplate.project.accessible_by(joe, {'read': True}) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in check_jobtemplate.read_role + assert admin in check_jobtemplate.execute_role + assert joe not in check_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role @pytest.mark.django_db @@ -120,17 +120,17 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert deploy_jobtemplate.project.accessible_by(joe, {'read': True}) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in deploy_jobtemplate.read_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in deploy_jobtemplate.execute_role + assert joe in deploy_jobtemplate.execute_role - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role @mock.patch.object(BaseAccess, 'check_license', return_value=None) diff --git a/awx/main/tests/functional/test_rbac_organization.py b/awx/main/tests/functional/test_rbac_organization.py index 89a0298df6..77558c0e7c 100644 --- a/awx/main/tests/functional/test_rbac_organization.py +++ b/awx/main/tests/functional/test_rbac_organization.py @@ -16,11 +16,11 @@ def test_organization_migration_admin(organization, permissions, user): # Undo some automatic work that we're supposed to be testing with our migration organization.admin_role.members.remove(u) - assert not organization.accessible_by(u, permissions['admin']) + assert u not in organization.admin_role rbac.migrate_organization(apps, None) - assert organization.accessible_by(u, permissions['admin']) + assert u in organization.admin_role @pytest.mark.django_db def test_organization_migration_user(organization, permissions, user): @@ -29,11 +29,11 @@ def test_organization_migration_user(organization, permissions, user): # Undo some automatic work that we're supposed to be testing with our migration organization.member_role.members.remove(u) - assert not organization.accessible_by(u, permissions['auditor']) + assert u not in organization.read_role rbac.migrate_organization(apps, None) - assert organization.accessible_by(u, permissions['auditor']) + assert u in organization.read_role @mock.patch.object(BaseAccess, 'check_license', return_value=None) diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index 6fae236667..d2e504645a 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -138,11 +138,11 @@ def test_project_user_project(user_project, project, user): assert old_access.check_user_access(u, user_project.__class__, 'read', user_project) assert old_access.check_user_access(u, project.__class__, 'read', project) is False - assert user_project.accessible_by(u, {'read': True}) is False - assert project.accessible_by(u, {'read': True}) is False + assert u not in user_project.read_role + assert u not in project.read_role rbac.migrate_projects(apps, None) - assert user_project.accessible_by(u, {'read': True}) is True - assert project.accessible_by(u, {'read': True}) is False + assert u in user_project.read_role + assert u not in project.read_role @pytest.mark.django_db def test_project_accessible_by_sa(user, project): @@ -150,21 +150,21 @@ def test_project_accessible_by_sa(user, project): # This gets setup by a signal, but we want to test the migration which will set this up too, so remove it Role.singleton('System Administrator').members.remove(u) - assert project.accessible_by(u, {'read': True}) is False + assert u not in project.read_role rbac.migrate_organization(apps, None) rbac.migrate_users(apps, None) rbac.migrate_projects(apps, None) print(project.admin_role.ancestors.all()) print(project.admin_role.ancestors.all()) - assert project.accessible_by(u, {'read': True, 'write': True}) is True + assert u in project.admin_role @pytest.mark.django_db def test_project_org_members(user, organization, project): admin = user('orgadmin') member = user('orgmember') - assert project.accessible_by(admin, {'read': True}) is False - assert project.accessible_by(member, {'read': True}) is False + assert admin not in project.read_role + assert member not in project.read_role organization.deprecated_admins.add(admin) organization.deprecated_users.add(member) @@ -172,8 +172,8 @@ def test_project_org_members(user, organization, project): rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(admin, {'read': True, 'write': True}) is True - assert project.accessible_by(member, {'read': True}) + assert admin in project.admin_role + assert member in project.read_role @pytest.mark.django_db def test_project_team(user, team, project): @@ -183,15 +183,15 @@ def test_project_team(user, team, project): team.deprecated_users.add(member) project.deprecated_teams.add(team) - assert project.accessible_by(nonmember, {'read': True}) is False - assert project.accessible_by(member, {'read': True}) is False + assert nonmember not in project.read_role + assert member not in project.read_role rbac.migrate_team(apps, None) rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(member, {'read': True}) is True - assert project.accessible_by(nonmember, {'read': True}) is False + assert member in project.read_role + assert nonmember not in project.read_role @pytest.mark.django_db def test_project_explicit_permission(user, team, project, organization): @@ -203,9 +203,9 @@ def test_project_explicit_permission(user, team, project, organization): p = Permission(user=u, project=project, permission_type='create', name='Perm name') p.save() - assert project.accessible_by(u, {'read': True}) is False + assert u not in project.read_role rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(u, {'read': True}) is True + assert u in project.read_role diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index a6ad507e22..7bd60279ca 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -54,11 +54,11 @@ def test_team_accessible_by(team, user, project): u = user('team_member', False) team.member_role.children.add(project.member_role) - assert project.accessible_by(team, {'read':True}) - assert not project.accessible_by(u, {'read':True}) + assert team in project.read_role + assert u not in project.read_role team.member_role.members.add(u) - assert project.accessible_by(u, {'read':True}) + assert u in project.read_role @pytest.mark.django_db def test_team_accessible_objects(team, user, project): diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 346413b6f6..9bfafe43f5 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -55,13 +55,13 @@ def test_org_user_admin(user, organization): member = user('orgmember') organization.member_role.members.add(member) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role organization.admin_role.members.add(admin) - assert member.accessible_by(admin, {'write':True}) + assert admin in member.admin_role organization.admin_role.members.remove(admin) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role @pytest.mark.django_db def test_org_user_removed(user, organization): @@ -71,7 +71,7 @@ def test_org_user_removed(user, organization): organization.admin_role.members.add(admin) organization.member_role.members.add(member) - assert member.accessible_by(admin, {'write':True}) + assert admin in member.admin_role organization.member_role.members.remove(member) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role From b25894aae87c9e29d4e630e3855e35137b9d1674 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 12:19:06 -0400 Subject: [PATCH 11/56] New field name fixes --- awx/main/models/inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index cad8395ca8..c50f5c8402 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -527,11 +527,11 @@ class Group(CommonModelNameNotUnique, ResourceMixin): ) update_role = ImplicitRoleField( role_name='Inventory Group Updater', - parent_role=['inventory.update_role', 'parents.updater_role'], + parent_role=['inventory.update_role', 'parents.update_role'], ) execute_role = ImplicitRoleField( role_name='Inventory Group Executor', - parent_role=['inventory.execute_role', 'parents.executor_role'], + parent_role=['inventory.execute_role', 'parents.execute_role'], ) def __unicode__(self): From 128a4f1823365cfbec9281897ca719dd56cb79c8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 14:33:09 -0400 Subject: [PATCH 12/56] Added read_role to inventory, group, and job templates --- awx/main/models/inventory.py | 9 +++++++++ awx/main/models/jobs.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index c50f5c8402..f1005268cd 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -118,6 +118,11 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory Executor', role_description='May execute jobs against this inventory', ) + read_role = ImplicitRoleField( + role_name='Read', + parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'], + role_description='May view this inventory', + ) def get_absolute_url(self): return reverse('api:inventory_detail', args=(self.pk,)) @@ -533,6 +538,10 @@ class Group(CommonModelNameNotUnique, ResourceMixin): role_name='Inventory Group Executor', parent_role=['inventory.execute_role', 'parents.execute_role'], ) + read_role = ImplicitRoleField( + role_name='Inventory Group Executor', + parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'], + ) def __unicode__(self): return self.name diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0cc3cc73af..c4a68f8e76 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -217,6 +217,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): role_name='Job Template Runner', role_description='May run the job template', ) + read_role = ImplicitRoleField( + role_name='Job Template Runner', + role_description='May run the job template', + parent_role=['execute_role', 'auditor_role', 'admin_role'], + ) @classmethod def _get_unified_job_class(cls): From fa10d562c13e4a1910f0f3f7468b4ca88d529416 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 14:36:52 -0400 Subject: [PATCH 13/56] Replaced get user permissions with get_roles_on_resource --- awx/api/serializers.py | 6 +-- awx/api/views.py | 2 +- awx/main/access.py | 4 +- awx/main/models/mixins.py | 47 +++------------------- awx/main/models/organization.py | 7 +++- awx/main/models/rbac.py | 27 ++++++++++++- awx/main/tests/functional/test_rbac_api.py | 15 +++---- 7 files changed, 52 insertions(+), 56 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bda63004c4..fa6f56fe2a 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1485,7 +1485,7 @@ class ResourceAccessListElementSerializer(UserSerializer): if 'summary_fields' not in ret: ret['summary_fields'] = {} - ret['summary_fields']['permissions'] = get_user_permissions_on_resource(obj, user) + ret['summary_fields']['permissions'] = get_roles_on_resource(obj, user) def format_role_perm(role): role_dict = { 'id': role.id, 'name': role.name, 'description': role.description} @@ -1495,7 +1495,7 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_role_permissions_on_resource(obj, role)} + return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, role)} def format_team_role_perm(team_role, permissive_role_ids): role = team_role.children.filter(id__in=permissive_role_ids)[0] @@ -1513,7 +1513,7 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_role_permissions_on_resource(obj, team_role)} + return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, team_role)} team_content_type = ContentType.objects.get_for_model(Team) content_type = ContentType.objects.get_for_model(obj) diff --git a/awx/api/views.py b/awx/api/views.py index 9831ecac03..1c56b129db 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -667,7 +667,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView): org_id = int(self.kwargs['pk']) org_counts = {} - access_kwargs = {'accessor': self.request.user, 'permissions': {"read": True}} + access_kwargs = {'accessor': self.request.user, 'role_name': 'read_role'} direct_counts = Organization.objects.filter(id=org_id).annotate( users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True) diff --git a/awx/main/access.py b/awx/main/access.py index 55d43598ef..5e984b811c 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -65,8 +65,8 @@ def register_access(model_class, access_class): def user_admin_role(self): return Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=self.id) -def user_accessible_objects(user, role): - return ResourceMixin._accessible_objects(User, user, role) +def user_accessible_objects(user, role_name): + return ResourceMixin._accessible_objects(User, user, role_name) def get_user_queryset(user, model_class): ''' diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 5fd7dc89f8..7ca3fa7db1 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -1,13 +1,11 @@ # Django from django.db import models -from django.db.models.aggregates import Max -from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User # noqa # AWX from awx.main.models.rbac import ( - Role, + Role, get_roles_on_resource ) @@ -55,45 +53,12 @@ class ResourceMixin(models.Model): return qs - def get_permissions(self, user): + def get_permissions(self, accessor): ''' - Returns a dict (or None) of the permissions a user has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to the roles that are applicable for the given - user. - - In example, if a user has been granted read access through a permission - on one role and write access through a permission on a separate role, - the returned dict will denote that the user has both read and write - access. + Returns a dict (or None) of the roles a accessor has for a given resource. + An accessor can be either a User, Role, or an arbitrary resource that + contains one or more Roles associated with it. ''' - return get_user_permissions_on_resource(self, user) + return get_roles_on_resource(self, accessor) - - def get_role_permissions(self, role): - ''' - Returns a dict (or None) of the permissions a role has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to either the role or any descendents of that role. - ''' - - return get_role_permissions_on_resource(self, role) - - - def accessible_by(self, user, permissions): - ''' - Returns true if the user has all of the specified permissions - ''' - - perms = self.get_permissions(user) - if perms is None: - return False - for k in permissions: - if k not in perms or perms[k] < permissions[k]: - return False - return True diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 7bbe1ed79f..9ef93e0b98 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -70,7 +70,7 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): read_role = ImplicitRoleField( role_name='Organization Read Access', role_description='Read an organization', - parent_role='member_role', + parent_role=['member_role', 'auditor_role'], ) @@ -124,6 +124,11 @@ class Team(CommonModelNameNotUnique, ResourceMixin): role_description='A member of this team', parent_role='admin_role', ) + read_role = ImplicitRoleField( + role_name='Read', + role_description='Can view this team', + parent_role=['auditor_role', 'member_role'], + ) def get_absolute_url(self): return reverse('api:team_detail', args=(self.pk,)) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index a9898f3441..75564c345c 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -9,7 +9,6 @@ import contextlib # Django from django.db import models, transaction, connection from django.db.models import Q -from django.db.models.aggregates import Max from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType @@ -22,6 +21,7 @@ from awx.main.models.base import * # noqa __all__ = [ 'Role', 'batch_role_ancestor_rebuilding', + 'get_roles_on_resource', 'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR', 'ROLE_SINGLETON_SYSTEM_AUDITOR', ] @@ -345,3 +345,28 @@ class Role(CommonModelNameNotUnique): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() + + + +def get_roles_on_resource(resource, accessor): + ''' + Returns a dict (or None) of the roles a accessor has for a given resource. + An accessor can be either a User, Role, or an arbitrary resource that + contains one or more Roles associated with it. + ''' + + if type(accessor) == User: + roles = accessor.roles.all() + elif type(accessor) == Role: + roles = accessor + else: + accessor_type = ContentType.objects.get_for_model(accessor) + roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) + + return { role.role_field: True for role in + Role.objects.filter( + content_type = ContentType.objects.get_for_model(resource), + object_id = resource.id, + ancestors = roles)} + diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index 9a3257875c..1fc1485e14 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -176,8 +176,9 @@ def test_get_teams_roles_list(get, team, organization, admin): response = get(url, admin) assert response.status_code == 200 roles = response.data - assert roles['count'] == 1 - assert roles['results'][0]['id'] == organization.admin_role.id + + assert roles['count'] == 2 + assert roles['results'][0]['id'] == organization.admin_role.id or roles['results'][1]['id'] == organization.admin_role.id @pytest.mark.django_db @@ -291,7 +292,7 @@ def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemp assert joe in check_jobtemplate.execute_role post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) - assert joe not in check_jobtemplate.execute + assert joe not in check_jobtemplate.execute_role @pytest.mark.django_db(transaction=True) @@ -317,7 +318,7 @@ def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobt check_jobtemplate.execute_role.members.add(joe) assert rando not in check_jobtemplate.admin_role - assert joe not in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, rando) assert res.status_code == 403 @@ -380,8 +381,8 @@ def test_role_children(get, team, admin, role): url = reverse('api:role_children_list', args=(team.member_role.id,)) response = get(url, admin) assert response.status_code == 200 - assert response.data['count'] == 1 - assert response.data['results'][0]['id'] == role.id + assert response.data['count'] == 2 + assert response.data['results'][0]['id'] == role.id or response.data['results'][1]['id'] == role.id @@ -417,7 +418,7 @@ def test_ensure_permissions_is_present(organization, get, user): assert 'summary_fields' in org assert 'permissions' in org['summary_fields'] - assert org['summary_fields']['permissions']['read'] > 0 + assert org['summary_fields']['permissions']['read_role'] > 0 @pytest.mark.django_db def test_ensure_role_summary_is_present(organization, get, user): From d9538b200a34eb3f72c81166d1413bb835cb4fd8 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 14:57:01 -0400 Subject: [PATCH 14/56] fix User.accessible_objects --- awx/main/migrations/_rbac.py | 10 ---------- awx/main/models/mixins.py | 18 +++++++++++++++++- awx/main/tests/functional/test_rbac_user.py | 6 +++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 04009a7453..4f37cf4c14 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -34,7 +34,6 @@ def init_rbac_migration(apps, schema_editor): def migrate_users(apps, schema_editor): User = apps.get_model('auth', "User") Role = apps.get_model('main', "Role") - RolePermission = apps.get_model('main', "RolePermission") ContentType = apps.get_model('contenttypes', "ContentType") user_content_type = ContentType.objects.get_for_model(User) @@ -52,15 +51,6 @@ def migrate_users(apps, schema_editor): object_id = user.id ) role.members.add(user) - RolePermission.objects.create( - created=now(), - modified=now(), - role = role, - content_type = user_content_type, - object_id = user.id, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) logger.info(smart_text(u"migrating to new role for user: {}".format(user.username))) if user.is_superuser: diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 7ca3fa7db1..a4af969434 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -33,6 +33,23 @@ class ResourceMixin(models.Model): @staticmethod def _accessible_objects(cls, accessor, role_name): + if type(cls()) == User: + cls_type = ContentType.objects.get_for_model(cls) + roles = Role.objects.filter(content_type__pk=cls_type.id) + + if type(accessor) == User: + roles = roles.filter(ancestors__members = accessor) + elif type(accessor) == Role: + roles = roles.filter(ancestors = accessor) + else: + accessor_type = ContentType.objects.get_for_model(accessor) + accessor_roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) + roles = roles.filter(ancestors__in=accessor_roles) + + kwargs = {'id__in':roles.values_list('object_id', flat=True)} + return cls.objects.filter(**kwargs) + if type(accessor) == User: kwargs = {} kwargs[role_name + '__ancestors__members'] = accessor @@ -49,7 +66,6 @@ class ResourceMixin(models.Model): kwargs[role_name + '__ancestors__in'] = roles qs = cls.objects.filter(**kwargs) - #return cls.objects.filter(resource__in=qs) return qs diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 9bfafe43f5..8e620771f5 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -40,14 +40,14 @@ def test_user_queryset(user): def test_user_accessible_objects(user, organization): admin = user('admin', False) u = user('john', False) - assert User.accessible_objects(admin, {'read':True}).count() == 1 + assert User.accessible_objects(admin, 'admin_role').count() == 1 organization.member_role.members.add(u) organization.admin_role.members.add(admin) - assert User.accessible_objects(admin, {'read':True}).count() == 2 + assert User.accessible_objects(admin, 'admin_role').count() == 2 organization.member_role.members.remove(u) - assert User.accessible_objects(admin, {'read':True}).count() == 1 + assert User.accessible_objects(admin, 'admin_role').count() == 1 @pytest.mark.django_db def test_org_user_admin(user, organization): From a361f1426642ec1845aefa69f04672db2d4e90c2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 14:57:08 -0400 Subject: [PATCH 15/56] Added adhoc_role to inventory and group, read_role to CustomInventorySource --- awx/main/models/inventory.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f1005268cd..0d052cb214 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -114,9 +114,14 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', ) + adhoc_role = ImplicitRoleField( + role_name='Inventory Ad Hoc', + role_description='May execute ad hoc commands against this inventory', + ) execute_role = ImplicitRoleField( role_name='Inventory Executor', role_description='May execute jobs against this inventory', + parent_role='adhoc_role', ) read_role = ImplicitRoleField( role_name='Read', @@ -534,9 +539,14 @@ class Group(CommonModelNameNotUnique, ResourceMixin): role_name='Inventory Group Updater', parent_role=['inventory.update_role', 'parents.update_role'], ) + adhoc_role = ImplicitRoleField( + role_name='Inventory Ad Hoc', + parent_role=['inventory.adhoc_role', 'parents.adhoc_role'], + role_description='May execute ad hoc commands against this inventory', + ) execute_role = ImplicitRoleField( role_name='Inventory Group Executor', - parent_role=['inventory.execute_role', 'parents.execute_role'], + parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'], ) read_role = ImplicitRoleField( role_name='Inventory Group Executor', @@ -1309,6 +1319,11 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): role_description='May view but not modify this inventory', parent_role='organization.auditor_role', ) + read_role = ImplicitRoleField( + role_name='CustomInventory Read', + role_description='May view but not modify this inventory', + parent_role=['auditor_role', 'member_role', 'admin_role'], + ) def get_absolute_url(self): return reverse('api:inventory_script_detail', args=(self.pk,)) From 2ddac6cb642ec952e97be79984536594e21f6bba Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 15:07:50 -0400 Subject: [PATCH 16/56] Fixed up some role parenting --- awx/main/models/inventory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 0d052cb214..963fe5b8da 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -109,14 +109,17 @@ class Inventory(CommonModel, ResourceMixin): update_role = ImplicitRoleField( role_name='Inventory Updater', role_description='May update the inventory', + parent_role=['admin_role'], ) use_role = ImplicitRoleField( role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', + parent_role=['admin_role'], ) adhoc_role = ImplicitRoleField( role_name='Inventory Ad Hoc', role_description='May execute ad hoc commands against this inventory', + parent_role=['admin_role'], ) execute_role = ImplicitRoleField( role_name='Inventory Executor', @@ -537,11 +540,11 @@ class Group(CommonModelNameNotUnique, ResourceMixin): ) update_role = ImplicitRoleField( role_name='Inventory Group Updater', - parent_role=['inventory.update_role', 'parents.update_role'], + parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'], ) adhoc_role = ImplicitRoleField( role_name='Inventory Ad Hoc', - parent_role=['inventory.adhoc_role', 'parents.adhoc_role'], + parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'], role_description='May execute ad hoc commands against this inventory', ) execute_role = ImplicitRoleField( From 8f70884c2aac490577f27203e4b554c7165247eb Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 15:08:12 -0400 Subject: [PATCH 17/56] role check fix --- awx/main/access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index 5e984b811c..f5bb2a52e3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -348,7 +348,7 @@ class InventoryAccess(BaseAccess): return self.can_admin(obj, None) def can_run_ad_hoc_commands(self, obj): - return self.user in adhoc_role + return self.user in obj.adhoc_role class HostAccess(BaseAccess): ''' From 8653b61cc067ded2886d7267208eef4fba607483 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 15:08:40 -0400 Subject: [PATCH 18/56] accessible_by -> in changes in test_rbac_inventory.py --- .../tests/functional/test_rbac_inventory.py | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 4434e199a0..b1cc4e81bd 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -16,10 +16,10 @@ def test_custom_inv_script_access(organization, user): custom_inv = CustomInventoryScript.objects.create(name='test', script='test', description='test') custom_inv.organization = organization custom_inv.save() - assert not custom_inv.accessible_by(u, {'read':True}) + assert u not in custom_inv.read_role organization.member_role.members.add(u) - assert custom_inv.accessible_by(u, {'read':True}) + assert u in custom_inv.read_role @pytest.mark.django_db def test_inventory_admin_user(inventory, permissions, user): @@ -27,11 +27,11 @@ def test_inventory_admin_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='admin') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False + assert u not in inventory.admin_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) + assert u in inventory.admin_role assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False @@ -41,13 +41,13 @@ def test_inventory_auditor_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='read') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is True + assert u not in inventory.admin_role + assert u in inventory.auditor_role assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False @@ -57,12 +57,12 @@ def test_inventory_updater_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='write') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False + assert u not in inventory.admin_role assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() @@ -72,13 +72,13 @@ def test_inventory_executor_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='read', run_ad_hoc_commands=True) perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is True + assert u not in inventory.admin_role + assert u in inventory.read_role assert inventory.execute_role.members.filter(id=u.id).exists() assert inventory.update_role.members.filter(id=u.id).exists() is False @@ -91,7 +91,7 @@ def test_inventory_admin_team(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False + assert u not in inventory.admin_role rbac.migrate_team(apps, None) rbac.migrate_inventory(apps, None) @@ -101,8 +101,8 @@ def test_inventory_admin_team(inventory, permissions, user, team): assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False - assert inventory.accessible_by(u, permissions['auditor']) - assert inventory.accessible_by(u, permissions['admin']) + assert u in inventory.read_role + assert u in inventory.admin_role @pytest.mark.django_db @@ -112,8 +112,8 @@ def test_inventory_auditor(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps,None) rbac.migrate_inventory(apps, None) @@ -123,8 +123,8 @@ def test_inventory_auditor(inventory, permissions, user, team): assert inventory.auditor_role.members.filter(id=u.id).exists() is False assert inventory.execute_role.members.filter(id=u.id).exists() is False assert inventory.update_role.members.filter(id=u.id).exists() is False - assert inventory.accessible_by(u, permissions['auditor']) - assert inventory.accessible_by(u, permissions['admin']) is False + assert u in inventory.read_role + assert u not in inventory.admin_role @pytest.mark.django_db def test_inventory_updater(inventory, permissions, user, team): @@ -133,8 +133,8 @@ def test_inventory_updater(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps,None) rbac.migrate_inventory(apps, None) @@ -155,8 +155,8 @@ def test_inventory_executor(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps, None) rbac.migrate_inventory(apps, None) @@ -177,21 +177,21 @@ def test_group_parent_admin(group, permissions, user): childA = group('child-1') parent1.admin_role.members.add(u) - assert parent1.accessible_by(u, permissions['admin']) - assert not parent2.accessible_by(u, permissions['admin']) - assert not childA.accessible_by(u, permissions['admin']) + assert u in parent1.admin_role + assert u not in parent2.admin_role + assert u not in childA.admin_role childA.parents.add(parent1) - assert childA.accessible_by(u, permissions['admin']) + assert u in childA.admin_role childA.parents.remove(parent1) - assert not childA.accessible_by(u, permissions['admin']) + assert u not in childA.admin_role parent2.children.add(childA) - assert not childA.accessible_by(u, permissions['admin']) + assert u not in childA.admin_role parent2.admin_role.members.add(u) - assert childA.accessible_by(u, permissions['admin']) + assert u in childA.admin_role @pytest.mark.django_db def test_access_admin(organization, inventory, user): From d2a81f46e3a16d32154482e4eaaee2000454a771 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 15:16:47 -0400 Subject: [PATCH 19/56] Fixed up last test case for host access --- awx/main/access.py | 2 +- .../tests/functional/test_rbac_inventory.py | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index f5bb2a52e3..1f9ee7aa66 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -368,7 +368,7 @@ class HostAccess(BaseAccess): return qs.prefetch_related('groups').all() def can_read(self, obj): - return obj and self.user in obj.read_role + return obj and any(self.user in grp.read_role for grp in obj.groups.all()) or self.user in obj.inventory.read_role def can_add(self, data): if not data or 'inventory' not in data: diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index b1cc4e81bd..0727b73e10 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -6,7 +6,7 @@ from awx.main.models import ( Host, CustomInventoryScript, ) -from awx.main.access import InventoryAccess +from awx.main.access import InventoryAccess, HostAccess from django.apps import apps @pytest.mark.django_db @@ -237,33 +237,35 @@ def test_host_access(organization, inventory, user, group): not_my_group = group('not-my-group') group_admin = user('group_admin', False) + inventory_admin_access = HostAccess(inventory_admin) + group_admin_access = HostAccess(group_admin) h1 = Host.objects.create(inventory=inventory, name='host1') h2 = Host.objects.create(inventory=inventory, name='host2') h1.groups.add(my_group) h2.groups.add(not_my_group) - assert h1.accessible_by(inventory_admin, {'read': True}) is False - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) is False + assert group_admin_access.can_read(h1) is False inventory.admin_role.members.add(inventory_admin) my_group.admin_role.members.add(group_admin) - assert h1.accessible_by(inventory_admin, {'read': True}) - assert h2.accessible_by(inventory_admin, {'read': True}) - assert h1.accessible_by(group_admin, {'read': True}) - assert h2.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) + assert inventory_admin_access.can_read(h2) + assert group_admin_access.can_read(h1) + assert group_admin_access.can_read(h2) is False my_group.hosts.remove(h1) - assert h1.accessible_by(inventory_admin, {'read': True}) - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) + assert group_admin_access.can_read(h1) is False h1.inventory = other_inventory h1.save() - assert h1.accessible_by(inventory_admin, {'read': True}) is False - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) is False + assert group_admin_access.can_read(h1) is False From 6229e978e9ef4a9a6873aaecd5371c859b22c13b Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 15:26:05 -0400 Subject: [PATCH 20/56] fix team tests --- awx/main/models/mixins.py | 4 ++-- awx/main/models/rbac.py | 5 +++++ awx/main/tests/functional/test_rbac_team.py | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index a4af969434..a54f49dfd6 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -48,7 +48,7 @@ class ResourceMixin(models.Model): roles = roles.filter(ancestors__in=accessor_roles) kwargs = {'id__in':roles.values_list('object_id', flat=True)} - return cls.objects.filter(**kwargs) + return cls.objects.filter(**kwargs).distinct() if type(accessor) == User: kwargs = {} @@ -66,7 +66,7 @@ class ResourceMixin(models.Model): kwargs[role_name + '__ancestors__in'] = roles qs = cls.objects.filter(**kwargs) - return qs + return qs.distinct() def get_permissions(self, accessor): diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 75564c345c..0f07e02478 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -94,6 +94,11 @@ class Role(CommonModelNameNotUnique): return reverse('api:role_detail', args=(self.pk,)) def __contains__(self, user): + if user.__class__.__name__ == 'Team': + team_type = ContentType.objects.get_for_model(user) + roles = Role.objects.filter(content_type__pk=team_type.id, + object_id=user.id) + return self.ancestors.filter(pk__in=roles).exists() return self.ancestors.filter(members=user).exists() def rebuild_role_ancestor_list(self): diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index 7bd60279ca..3961cb837a 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -65,9 +65,9 @@ def test_team_accessible_objects(team, user, project): u = user('team_member', False) team.member_role.children.add(project.member_role) - assert len(Project.accessible_objects(team, {'read':True})) == 1 - assert not Project.accessible_objects(u, {'read':True}) + assert len(Project.accessible_objects(team, 'read_role')) == 1 + assert not Project.accessible_objects(u, 'read_role') team.member_role.members.add(u) - assert len(Project.accessible_objects(u, {'read':True})) == 1 + assert len(Project.accessible_objects(u, 'read_role')) == 1 From c3923da37aae2ea913c5af34eca76fb8a159d000 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 15 Apr 2016 15:31:31 -0400 Subject: [PATCH 21/56] fixing credential access --- awx/main/access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index 1f9ee7aa66..32e600fbe9 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -558,7 +558,7 @@ class CredentialAccess(BaseAccess): def can_change(self, obj, data): if self.user.is_superuser: return True - return self.user in obj.admin_role + return self.user in obj.owner_role def can_delete(self, obj): # Unassociated credentials may be marked deleted by anyone, though we From 2979f6e6d34c020a7deb92f30046cec31c670000 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 15:55:59 -0400 Subject: [PATCH 22/56] jobtemplate execute_role is now child of admin_role --- awx/main/models/jobs.py | 1 + awx/main/models/rbac.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index c4a68f8e76..103e32397e 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -216,6 +216,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): execute_role = ImplicitRoleField( role_name='Job Template Runner', role_description='May run the job template', + parent_role=['admin_role'], ) read_role = ImplicitRoleField( role_name='Job Template Runner', diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 0f07e02478..846e49f8f0 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -93,13 +93,21 @@ class Role(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) - def __contains__(self, user): - if user.__class__.__name__ == 'Team': - team_type = ContentType.objects.get_for_model(user) - roles = Role.objects.filter(content_type__pk=team_type.id, - object_id=user.id) + def __contains__(self, accessor): + if type(accessor) == User: + return self.ancestors.filter(members=accessor).exists() + elif accessor.__class__.__name__ == 'Team': + return self.ancestors.filter(pk=accessor.member_role.id).exists() + elif type(accessor) == Role: + return self.ancestors.filter(pk=accessor).exists() + else: + accessor_type = ContentType.objects.get_for_model(accessor) + roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) return self.ancestors.filter(pk__in=roles).exists() - return self.ancestors.filter(members=user).exists() + + + def rebuild_role_ancestor_list(self): ''' From c440aefd8544475eb4d1d3a13cd2f6ca990f6af0 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:08:14 -0400 Subject: [PATCH 23/56] fix jt tests --- awx/main/migrations/_rbac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 4f37cf4c14..0d7aa3ecb7 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -390,7 +390,7 @@ def migrate_job_templates(apps, schema_editor): jt.execute_role.members.add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) - if jt.accessible_by(user, {'execute': True}): + if user in jt.execute_role: # If the job template is already accessible by the user, because they # are a sytem, organization, or project admin, then don't add an explicit # role entry for them From 527be453a468a34e0f6242c53d2c4e604c6e33a3 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:11:44 -0400 Subject: [PATCH 24/56] fix last cred test --- awx/main/tests/functional/test_rbac_credential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 1bc584c5c9..e48b964bc8 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -22,7 +22,7 @@ def test_credential_migration_user(credential, user, permissions): def test_credential_use_role(credential, user, permissions): u = user('user', False) credential.use_role.members.add(u) - assert u in credential.owner_role + assert u in credential.use_role @pytest.mark.django_db def test_credential_migration_team_member(credential, team, user, permissions): From 8c8c8a624ff5ae9ac25d3b044ca27b4310ba1e51 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:18:17 -0400 Subject: [PATCH 25/56] Updates to views.py for RolePermission removal --- awx/api/views.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 1c56b129db..f4d7d4b3d3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -220,7 +220,7 @@ class ApiV1ConfigView(APIView): user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) data['user_ldap_fields'] = user_ldap_fields - if request.user.is_superuser or Organization.accessible_objects(request.user, {'write': True}).exists(): + if request.user.is_superuser or Organization.accessible_objects(request.user, 'admin_role').exists(): data.update(dict( project_base_dir = settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), @@ -566,7 +566,7 @@ class OrganizationList(ListCreateAPIView): serializer_class = OrganizationSerializer def get_queryset(self): - qs = Organization.accessible_objects(self.request.user, {'read': True}) + qs = Organization.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'auditor_role', 'member_role') return qs @@ -595,27 +595,27 @@ class OrganizationList(ListCreateAPIView): return full_context db_results = {} - org_qs = self.model.accessible_objects(self.request.user, {"read": True}) + org_qs = self.model.accessible_objects(self.request.user, 'read_role') org_id_list = org_qs.values('id') if len(org_id_list) == 0: if self.request.method == 'POST': full_context['related_field_counts'] = {} return full_context - inv_qs = Inventory.accessible_objects(self.request.user, {"read": True}) - project_qs = Project.accessible_objects(self.request.user, {"read": True}) + inv_qs = Inventory.accessible_objects(self.request.user, 'read_role') + project_qs = Project.accessible_objects(self.request.user, 'read_role') # Produce counts of Foreign Key relationships db_results['inventories'] = inv_qs\ .values('organization').annotate(Count('organization')).order_by('organization') db_results['teams'] = Team.accessible_objects( - self.request.user, {"read": True}).values('organization').annotate( + self.request.user, 'read_role').values('organization').annotate( Count('organization')).order_by('organization') JT_reference = 'project__organization' db_results['job_templates'] = JobTemplate.accessible_objects( - self.request.user, {"read": True}).values(JT_reference).annotate( + self.request.user, 'read_role').values(JT_reference).annotate( Count(JT_reference)).order_by(JT_reference) db_results['projects'] = project_qs\ @@ -784,7 +784,7 @@ class TeamList(ListCreateAPIView): serializer_class = TeamSerializer def get_queryset(self): - qs = Team.accessible_objects(self.request.user, {'read': True}) + qs = Team.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'auditor_role', 'member_role') return qs @@ -832,7 +832,7 @@ class TeamProjectsList(SubListAPIView): team = self.get_parent_object() self.check_parent_access(team) team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)) - user_qs = Project.accessible_objects(self.request.user, {'read': True}) + user_qs = Project.accessible_objects(self.request.user, 'read_role') return team_qs & user_qs @@ -860,8 +860,8 @@ class TeamActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(team=parent) | - Q(project__in=Project.accessible_objects(parent, {'read':True})) | - Q(credential__in=Credential.accessible_objects(parent, {'read':True}))) + Q(project__in=Project.accessible_objects(parent, 'read_role')) | + Q(credential__in=Credential.accessible_objects(parent, 'read_role'))) class TeamAccessList(ResourceAccessList): @@ -875,7 +875,7 @@ class ProjectList(ListCreateAPIView): serializer_class = ProjectSerializer def get_queryset(self): - projects_qs = Project.accessible_objects(self.request.user, {'read': True}) + projects_qs = Project.accessible_objects(self.request.user, 'read_role') projects_qs = projects_qs.select_related( 'organization', 'admin_role', @@ -1065,7 +1065,7 @@ class UserTeamsList(ListAPIView): u = get_object_or_404(User, pk=self.kwargs['pk']) if not self.request.user.can_access(User, 'read', u): raise PermissionDenied() - return Team.accessible_objects(self.request.user, {'read': True}).filter(member_role__members=u) + return Team.accessible_objects(self.request.user, 'read_role').filter(member_role__members=u) class UserRolesList(SubListCreateAttachDetachAPIView): @@ -1103,8 +1103,8 @@ class UserProjectsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Project.accessible_objects(self.request.user, {'read': True}) - user_qs = Project.accessible_objects(parent, {'read': True}) + my_qs = Project.accessible_objects(self.request.user, 'read_role') + user_qs = Project.accessible_objects(parent, 'read_role') return my_qs & user_qs class UserOrganizationsList(SubListAPIView): @@ -1117,7 +1117,7 @@ class UserOrganizationsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Organization.accessible_objects(self.request.user, {'read': True}) + my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(member_role__members=parent) return my_qs & user_qs @@ -1131,7 +1131,7 @@ class UserAdminOfOrganizationsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Organization.accessible_objects(self.request.user, {'read': True}) + my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(admin_role__members=parent) return my_qs & user_qs @@ -1217,7 +1217,7 @@ class CredentialList(ListCreateAPIView): organization = Organization.objects.get(pk=request.data['organization']) obj = organization - if not obj.accessible_by(self.request.user, {'write': True}): + if self.request.user not in obj.owner_role: raise PermissionDenied() ret = super(CredentialList, self).post(request, *args, **kwargs) @@ -1242,8 +1242,8 @@ class UserCredentialsList(CredentialList): if not self.request.user.can_access(User, 'read', user): raise PermissionDenied() - visible_creds = Credential.accessible_objects(self.request.user, {'read': True}) - user_creds = Credential.accessible_objects(user, {'read': True}) + visible_creds = Credential.accessible_objects(self.request.user, 'read_role') + user_creds = Credential.accessible_objects(user, 'read_role') return user_creds & visible_creds def post(self, request, *args, **kwargs): @@ -1262,7 +1262,7 @@ class TeamCredentialsList(CredentialList): if not self.request.user.can_access(Team, 'read', team): raise PermissionDenied() - visible_creds = Credential.accessible_objects(self.request.user, {'read': True}) + visible_creds = Credential.accessible_objects(self.request.user, 'read_role') team_creds = Credential.objects.filter(owner_role__parents=team.member_role) return team_creds & visible_creds @@ -1282,8 +1282,8 @@ class OrganizationCredentialList(CredentialList): if not self.request.user.can_access(Organization, 'read', organization): raise PermissionDenied() - user_visible = Credential.accessible_objects(self.request.user, {'read': True}).all() - org_set = Credential.accessible_objects(organization.admin_role, {'read': True}).all() + user_visible = Credential.accessible_objects(self.request.user, 'read_role').all() + org_set = Credential.accessible_objects(organization.admin_role, 'read_role').all() if self.request.user.is_superuser: return org_set @@ -1353,7 +1353,7 @@ class InventoryList(ListCreateAPIView): serializer_class = InventorySerializer def get_queryset(self): - qs = Inventory.accessible_objects(self.request.user, {'read': True}) + qs = Inventory.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'auditor_role', 'update_role', 'execute_role') return qs From 1fa70106d88e100690f9c7849b991fee78cee13b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:26:59 -0400 Subject: [PATCH 26/56] Added read_role to credential --- awx/main/models/credential.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 3d09cf462e..9892d8ff4a 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -186,6 +186,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): role_name='Credential User', role_description='May use this credential, but not read sensitive portions or modify it', ) + read_role = ImplicitRoleField( + role_name='Credential REad', + role_description='May read this credential', + parent_role=[ + 'use_role', 'auditor_role', 'owner_role' + ], + ) @property def needs_ssh_password(self): From d5bc4556779dfc019aad856c724338756e3b13be Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:27:24 -0400 Subject: [PATCH 27/56] admins can use credentials too --- awx/main/models/credential.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 9892d8ff4a..bd24a72de5 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -185,6 +185,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): use_role = ImplicitRoleField( role_name='Credential User', role_description='May use this credential, but not read sensitive portions or modify it', + parent_role=['owner_role'] ) read_role = ImplicitRoleField( role_name='Credential REad', From 5e7b6ed0847154a99f790766bbd7ccf4295118a3 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:29:22 -0400 Subject: [PATCH 28/56] fixes for api/test_credential.py --- awx/api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index f4d7d4b3d3..fdde047b33 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -831,8 +831,8 @@ class TeamProjectsList(SubListAPIView): def get_queryset(self): team = self.get_parent_object() self.check_parent_access(team) - team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)) - user_qs = Project.accessible_objects(self.request.user, 'read_role') + team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)).distinct() + user_qs = Project.accessible_objects(self.request.user, 'read_role').distinct() return team_qs & user_qs @@ -1217,7 +1217,7 @@ class CredentialList(ListCreateAPIView): organization = Organization.objects.get(pk=request.data['organization']) obj = organization - if self.request.user not in obj.owner_role: + if self.request.user not in obj.admin_role: raise PermissionDenied() ret = super(CredentialList, self).post(request, *args, **kwargs) @@ -1263,7 +1263,7 @@ class TeamCredentialsList(CredentialList): raise PermissionDenied() visible_creds = Credential.accessible_objects(self.request.user, 'read_role') - team_creds = Credential.objects.filter(owner_role__parents=team.member_role) + team_creds = Credential.objects.filter(owner_role__parents=team.member_role).distinct() return team_creds & visible_creds def post(self, request, *args, **kwargs): From 302774e85d0e767f22c94a6916c8a58a77fb5a68 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 16:47:12 -0400 Subject: [PATCH 29/56] Fixed up access_list functionality --- awx/api/generics.py | 5 ++++- awx/api/serializers.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 23dcce4d35..cdd10e497d 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -13,6 +13,7 @@ from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.utils.encoding import smart_text from django.utils.safestring import mark_safe +from django.contrib.contenttypes.models import ContentType # Django REST Framework from rest_framework.authentication import get_authorization_header @@ -475,7 +476,9 @@ class ResourceAccessList(ListAPIView): resource_model = getattr(self, 'resource_model') obj = resource_model.objects.get(pk=self.object_id) - roles = set([p.role for p in obj.role_permissions.all()]) + content_type = ContentType.objects.get_for_model(obj) + roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id)) + ancestors = set() for r in roles: ancestors.update(set(r.ancestors.all())) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index fa6f56fe2a..bce2484e23 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1518,8 +1518,10 @@ class ResourceAccessListElementSerializer(UserSerializer): team_content_type = ContentType.objects.get_for_model(Team) content_type = ContentType.objects.get_for_model(obj) - direct_permissive_role_ids = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).values_list('role__id') - all_permissive_role_ids = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).values_list('role__ancestors__id') + + content_type = ContentType.objects.get_for_model(obj) + direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True) + all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True) direct_access_roles = user.roles \ .filter(id__in=direct_permissive_role_ids).all() From 7bfc8cd43ba58ca414d408fe856420be638516e8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 15 Apr 2016 17:22:48 -0400 Subject: [PATCH 30/56] flake8 --- awx/main/models/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index b9ce674dff..f6c9702d4b 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -3,7 +3,6 @@ # Django from django.conf import settings # noqa -from django.contrib.contenttypes.fields import GenericRelation # AWX from awx.main.models.base import * # noqa From 7659da6162aa565e3676330917174851f377e91f Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 08:36:36 -0400 Subject: [PATCH 31/56] Fixes for old/ad_hoc.py tests --- awx/main/access.py | 2 +- awx/main/tests/old/ad_hoc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 32e600fbe9..c4c8f05517 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -361,7 +361,7 @@ class HostAccess(BaseAccess): def get_queryset(self): inv_qs = Inventory.accessible_objects(self.user, 'read_role') group_qs = Group.accessible_objects(self.user, 'read_role') - qs = (self.model.filter(inventory=inv_qs) | self.model.filter(group=group_qs)).distinct() + qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct() qs = qs.select_related('created_by', 'modified_by', 'inventory', 'last_job__job_template', 'last_job_host_summary__job') diff --git a/awx/main/tests/old/ad_hoc.py b/awx/main/tests/old/ad_hoc.py index f69d58072d..279b98f3ae 100644 --- a/awx/main/tests/old/ad_hoc.py +++ b/awx/main/tests/old/ad_hoc.py @@ -1025,7 +1025,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Update permission to allow other user to run ad hoc commands. Can # only see his own ad hoc commands (because of credential permission). with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.adhoc_role.id}, expect=204) with self.current_user('other'): response = self.get(url, expect=200) self.assertEqual(response['count'], 0) From 2c45f79298ea36e43d65fb083a6e216ff2358dd4 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 08:47:51 -0400 Subject: [PATCH 32/56] Migration updates for RolePermission removal --- awx/main/migrations/0008_v300_rbac_changes.py | 183 ++++++++++++------ 1 file changed, 127 insertions(+), 56 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index c912665970..3aba96526d 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -72,177 +72,248 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'roles', }, ), - migrations.CreateModel( - name='RolePermission', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(default=None, editable=False)), - ('modified', models.DateTimeField(default=None, editable=False)), - ('auto_generated', models.BooleanField(default=False)), - ('object_id', models.PositiveIntegerField(default=None)), - ('create', models.IntegerField(default=0)), - ('read', models.IntegerField(default=0)), - ('write', models.IntegerField(default=0)), - ('update', models.IntegerField(default=0)), - ('delete', models.IntegerField(default=0)), - ('execute', models.IntegerField(default=0)), - ('scm_update', models.IntegerField(default=0)), - ('use', models.IntegerField(default=0)), - ('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType')), - ('role', models.ForeignKey(related_name='permissions', to='main.Role')), - ], - options={ - 'db_table': 'main_rbac_permissions', - 'verbose_name_plural': 'permissions', - }, - ), migrations.AddField( model_name='credential', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True'), ), migrations.AddField( model_name='credential', name='owner_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True'), ), migrations.AddField( model_name='credential', name='use_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True', permissions={b'use': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True'), ), migrations.AddField( model_name='group', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True'), ), migrations.AddField( model_name='group', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True'), ), migrations.AddField( model_name='group', name='execute_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True', permissions={b'read': True, b'execute': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), ), migrations.AddField( model_name='group', name='update_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True', permissions={b'read': True, b'write': True, b'create': True, b'use': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'), ), migrations.AddField( model_name='inventory', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True'), ), migrations.AddField( model_name='inventory', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True'), ), migrations.AddField( model_name='inventory', name='execute_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True', permissions={b'read': True, b'execute': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True'), ), migrations.AddField( model_name='inventory', name='update_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True', permissions={b'read': True, b'update': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True'), ), migrations.AddField( model_name='inventory', name='use_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True', permissions={b'use': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True'), ), migrations.AddField( model_name='jobtemplate', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True'), ), migrations.AddField( model_name='jobtemplate', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True'), ), migrations.AddField( model_name='jobtemplate', name='execute_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True', permissions={b'read': True, b'execute': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True'), ), migrations.AddField( model_name='organization', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True'), ), migrations.AddField( model_name='organization', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True'), ), migrations.AddField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True'), ), migrations.AddField( model_name='project', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True'), ), migrations.AddField( model_name='project', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True'), ), migrations.AddField( model_name='project', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True'), ), migrations.AddField( model_name='project', name='scm_update_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True', permissions={b'scm_update': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True'), ), migrations.AddField( model_name='team', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True'), ), migrations.AddField( model_name='team', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True'), ), migrations.AddField( model_name='team', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', parent_role=b'admin_role', to='main.Role', role_name=b'Team Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', parent_role=b'admin_role', to='main.Role', role_name=b'Team Member', null=b'True'), + ), + + migrations.AddField( + model_name='credential', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read this credential', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', role_name=b'Credential REad', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', role_name=b'CustomInventory Read', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view this inventory', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', role_name=b'Read', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read an organization', parent_role=[b'member_role', b'auditor_role'], to='main.Role', role_name=b'Organization Read Access', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=b'member_role', to='main.Role', role_name=b'Project Read Access', null=b'True'), + ), + migrations.AddField( + model_name='role', + name='role_field', + field=models.TextField(default=b''), + ), + migrations.AddField( + model_name='team', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'), + ), + migrations.AlterField( + model_name='credential', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=[b'owner_role'], to='main.Role', role_name=b'Credential User', null=b'True'), + ), + migrations.AlterField( + model_name='group', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), + ), + migrations.AlterField( + model_name='group', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=b'adhoc_role', to='main.Role', role_name=b'Inventory Executor', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Updater', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory User', null=b'True'), + ), + migrations.AlterField( + model_name='jobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'), + ), + migrations.AlterField( + model_name='project', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=b'admin_role', to='main.Role', role_name=b'Project Member', null=b'True'), ), - migrations.AlterIndexTogether( - name='rolepermission', - index_together=set([('content_type', 'object_id')]), - ), + + + migrations.RenameField( model_name='organization', old_name='projects', From 6c50d0793d5c6d38e873e9eba3bea2ae10a5ad67 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 10:22:03 -0400 Subject: [PATCH 33/56] More test fixes --- awx/main/models/mixins.py | 2 +- awx/main/tests/old/inventory.py | 4 ++-- awx/main/tests/old/organizations.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index a54f49dfd6..1e91333b4a 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -22,7 +22,7 @@ class ResourceMixin(models.Model): Use instead of `MyModel.objects` when you want to only consider resources that a user has specific permissions for. For example: - MyModel.accessible_objects(user, {'read': True}).filter(name__istartswith='bar'); + MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar'); NOTE: This should only be used for list type things. If you have a specific resource you want to check permissions on, it is more diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py index 6d167ad10a..150eb45a31 100644 --- a/awx/main/tests/old/inventory.py +++ b/awx/main/tests/old/inventory.py @@ -77,7 +77,7 @@ class InventoryTest(BaseTest): self.check_get_list(url, self.normal_django_user, normal_qs) # a user who is on a team who has a read permissions on an inventory can see filtered inventories - other_qs = Inventory.accessible_objects(self.other_django_user, {'read': True}).distinct() + other_qs = Inventory.accessible_objects(self.other_django_user, 'read_role').distinct() self.check_get_list(url, self.other_django_user, other_qs) # a regular user not part of anything cannot see any inventories @@ -401,7 +401,7 @@ class InventoryTest(BaseTest): del_children_url = reverse('api:group_children_list', args=(del_group.pk,)) nondel_url = reverse('api:group_detail', args=(Group.objects.get(name='nondel').pk,)) - assert(inv.accessible_by(self.normal_django_user, {'read': True})) + assert self.normal_django_user in inv.read_role del_group.delete() nondel_detail = self.get(nondel_url, expect=200, auth=self.get_normal_credentials()) self.post(del_children_url, data=nondel_detail, expect=400, auth=self.get_normal_credentials()) diff --git a/awx/main/tests/old/organizations.py b/awx/main/tests/old/organizations.py index 1ace31a340..136e2603cc 100644 --- a/awx/main/tests/old/organizations.py +++ b/awx/main/tests/old/organizations.py @@ -289,7 +289,7 @@ class OrganizationsTest(BaseTest): # post a completely new user to verify we can add users to the subcollection directly new_user = dict(username='NewUser9000', password='NewPassword9000') - which_org = Organization.accessible_objects(self.normal_django_user, {'read': True, 'write': True})[0] + which_org = Organization.accessible_objects(self.normal_django_user, 'admin_role')[0] url = reverse('api:organization_users_list', args=(which_org.pk,)) self.post(url, new_user, expect=201, auth=self.get_normal_credentials()) From 25303cf4ec8d03e8c26d1e25653377ab4e76b524 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:24:26 -0400 Subject: [PATCH 34/56] Reverted user owner_role back to admin_role --- awx/main/signals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index bf0095073e..a6767177b0 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -126,12 +126,12 @@ def create_user_role(instance, **kwargs): Role.objects.get( content_type=ContentType.objects.get_for_model(instance), object_id=instance.id, - name = 'Owner' + name = 'User Admin' ) except Role.DoesNotExist: role = Role.objects.create( - name = 'Owner', - role_field='owner_role', + name = 'User Admin', + role_field='admin_role', content_object = instance, ) role.members.add(instance) From 5d0c6cc04408437b37caa5558a05e6a6ebd9fa96 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:27:57 -0400 Subject: [PATCH 35/56] Switch to custom ancestry table for some optimized queries Now we can stuff some more data in this table so we can take advantage of some multi-column indexing and avoid another to join for our accessible objects and permissions queries. --- awx/main/migrations/0008_v300_rbac_changes.py | 29 ++++- awx/main/models/mixins.py | 50 +++----- awx/main/models/rbac.py | 115 ++++++++++-------- 3 files changed, 109 insertions(+), 85 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 3aba96526d..79684e9a5f 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -48,6 +48,19 @@ class Migration(migrations.Migration): field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True), ), + migrations.CreateModel( + name='RoleAncestorEntry', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('role_field', models.TextField()), + ('content_type_id', models.PositiveIntegerField(null=False)), + ('object_id', models.PositiveIntegerField(null=False)), + ], + options={ + 'db_table': 'main_rbac_role_ancestors', + 'verbose_name_plural': 'role_ancestors', + }, + ), migrations.CreateModel( name='Role', fields=[ @@ -58,7 +71,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=512)), ('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)), ('object_id', models.PositiveIntegerField(default=None, null=True)), - ('ancestors', models.ManyToManyField(related_name='descendents', to='main.Role')), + ('ancestors', models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role')), ('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)), ('created_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), ('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)), @@ -72,6 +85,20 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'roles', }, ), + migrations.AddField( + model_name='roleancestorentry', + name='ancestor', + field=models.ForeignKey(related_name='+', to='main.Role'), + ), + migrations.AddField( + model_name='roleancestorentry', + name='descendent', + field=models.ForeignKey(related_name='+', to='main.Role'), + ), + migrations.AlterIndexTogether( + name='roleancestorentry', + index_together=set([('descendent', 'content_type_id', 'role_field'), ('ancestor', 'content_type_id', 'role_field')]), + ), migrations.AddField( model_name='credential', diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 1e91333b4a..45d259bacd 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User # noqa # AWX from awx.main.models.rbac import ( - Role, get_roles_on_resource + Role, RoleAncestorEntry, get_roles_on_resource ) @@ -17,7 +17,7 @@ class ResourceMixin(models.Model): abstract = True @classmethod - def accessible_objects(cls, accessor, role_name): + def accessible_objects(cls, accessor, role_field): ''' Use instead of `MyModel.objects` when you want to only consider resources that a user has specific permissions for. For example: @@ -29,44 +29,26 @@ class ResourceMixin(models.Model): performant to resolve the resource in question then call `myresource.get_permissions(user)`. ''' - return ResourceMixin._accessible_objects(cls, accessor, role_name) + return ResourceMixin._accessible_objects(cls, accessor, role_field) @staticmethod - def _accessible_objects(cls, accessor, role_name): - if type(cls()) == User: - cls_type = ContentType.objects.get_for_model(cls) - roles = Role.objects.filter(content_type__pk=cls_type.id) - - if type(accessor) == User: - roles = roles.filter(ancestors__members = accessor) - elif type(accessor) == Role: - roles = roles.filter(ancestors = accessor) - else: - accessor_type = ContentType.objects.get_for_model(accessor) - accessor_roles = Role.objects.filter(content_type__pk=accessor_type.id, - object_id=accessor.id) - roles = roles.filter(ancestors__in=accessor_roles) - - kwargs = {'id__in':roles.values_list('object_id', flat=True)} - return cls.objects.filter(**kwargs).distinct() - + def _accessible_objects(cls, accessor, role_field): if type(accessor) == User: - kwargs = {} - kwargs[role_name + '__ancestors__members'] = accessor - qs = cls.objects.filter(**kwargs) + ancestor_roles = accessor.roles.all() elif type(accessor) == Role: - kwargs = {} - kwargs[role_name + '__ancestors'] = accessor - qs = cls.objects.filter(**kwargs) + ancestor_roles = [accessor] else: accessor_type = ContentType.objects.get_for_model(accessor) - roles = Role.objects.filter(content_type__pk=accessor_type.id, - object_id=accessor.id) - kwargs = {} - kwargs[role_name + '__ancestors__in'] = roles - qs = cls.objects.filter(**kwargs) - - return qs.distinct() + ancestor_roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) + qs = cls.objects.filter(pk__in = + RoleAncestorEntry.objects.filter( + ancestor__in=ancestor_roles, + content_type_id = ContentType.objects.get_for_model(cls).id, + role_field = role_field + ).values_list('object_id').distinct() + ) + return qs def get_permissions(self, accessor): diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 846e49f8f0..d09d7d2dda 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -80,7 +80,12 @@ class Role(CommonModelNameNotUnique): role_field = models.TextField(null=False, default='') parents = models.ManyToManyField('Role', related_name='children') implicit_parents = models.TextField(null=False, default='[]') - ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list` + ancestors = models.ManyToManyField( + 'Role', + through='RoleAncestorEntry', + through_fields=('descendent', 'ancestor'), + related_name='descendents' + ) # auto-generated by `rebuild_role_ancestor_list` members = models.ManyToManyField('auth.User', related_name='roles') content_type = models.ForeignKey(ContentType, null=True, default=None) object_id = models.PositiveIntegerField(null=True, default=None) @@ -106,9 +111,6 @@ class Role(CommonModelNameNotUnique): object_id=accessor.id) return self.ancestors.filter(pk__in=roles).exists() - - - def rebuild_role_ancestor_list(self): ''' Updates our `ancestors` map to accurately reflect all of the ancestors for a role @@ -151,25 +153,6 @@ class Role(CommonModelNameNotUnique): Role._simultaneous_ancestry_rebuild([self.id]) - @staticmethod - def _simultaneous_ancestry_rebuild2(role_ids_to_rebuild): - #all_parents = Role.parents.through.values_list('to_role_id', 'from_role_id') - - ''' - sql_params = { - 'ancestors_table': Role.ancestors.through._meta.db_table, - 'parents_table': Role.parents.through._meta.db_table, - 'roles_table': Role._meta.db_table, - 'ids': ','.join(str(x) for x in role_ids_to_rebuild) - } - ''' - #Expand our parent list - #Expand our child list - #Construct our ancestor list for - #apply diff - - - @staticmethod def _simultaneous_ancestry_rebuild(role_ids_to_rebuild): # @@ -250,10 +233,6 @@ class Role(CommonModelNameNotUnique): # updated, and so we can terminate our loop. # # - # *NOTE* Keen reader may realize that there are many instances where - # fuck. cycles will never shake parents. - # - # cursor = connection.cursor() loop_ct = 0 @@ -274,10 +253,16 @@ class Role(CommonModelNameNotUnique): # TODO: Test to see if not deleting any entry that has a direct # correponding entry in the parents table helps reduce the processing # time significantly + """ cursor.execute(''' DELETE FROM %(ancestors_table)s - WHERE to_role_id IN (%(ids)s) - AND from_role_id != to_role_id + WHERE ancestor_id IN (%(ids)s) + AND descendent_id != ancestor_id + ''' % sql_params) + """ + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE ancestor_id IN (%(ids)s) ''' % sql_params) @@ -297,42 +282,52 @@ class Role(CommonModelNameNotUnique): cursor.execute(''' DELETE FROM %(ancestors_table)s - WHERE from_role_id IN (%(ids)s) + WHERE descendent_id IN (%(ids)s) AND id NOT IN ( SELECT %(ancestors_table)s.id FROM ( - SELECT parents.from_role_id from_id, ancestors.to_role_id to_id + SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id FROM %(parents_table)s as parents LEFT JOIN %(ancestors_table)s as ancestors - ON (parents.to_role_id = ancestors.from_role_id) - WHERE parents.from_role_id IN (%(ids)s) AND ancestors.to_role_id IS NOT NULL + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL UNION SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) ) new_ancestry_list - LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.from_role_id - AND new_ancestry_list.to_id = %(ancestors_table)s.to_role_id) + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) WHERE %(ancestors_table)s.id IS NOT NULL ) ''' % sql_params) delete_ct = cursor.rowcount cursor.execute(''' - INSERT INTO %(ancestors_table)s (from_role_id, to_role_id) - SELECT from_id, to_id FROM ( - SELECT parents.from_role_id from_id, ancestors.to_role_id to_id + INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id) + SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM ( + SELECT parents.from_role_id from_id, + ancestors.ancestor_id to_id, + roles.role_field, + COALESCE(roles.content_type_id, 0) content_type_id, + COALESCE(roles.object_id, 0) object_id FROM %(parents_table)s as parents - LEFT JOIN %(ancestors_table)s as ancestors - ON (parents.to_role_id = ancestors.from_role_id) - WHERE parents.from_role_id IN (%(ids)s) AND ancestors.to_role_id IS NOT NULL + INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id) + LEFT OUTER JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL UNION - SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) + SELECT id from_id, + id to_id, + role_field, + COALESCE(content_type_id, 0) content_type_id, + COALESCE(object_id, 0) object_id + from %(roles_table)s WHERE id IN (%(ids)s) ) new_ancestry_list - LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.from_role_id - AND new_ancestry_list.to_id = %(ancestors_table)s.to_role_id) + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) WHERE %(ancestors_table)s.id IS NULL ''' % sql_params) insert_ct = cursor.rowcount @@ -358,7 +353,24 @@ class Role(CommonModelNameNotUnique): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() +class RoleAncestorEntry(models.Model): + class Meta: + app_label = 'main' + verbose_name_plural = _('role_ancestors') + db_table = 'main_rbac_role_ancestors' + index_together = [ + ("ancestor", "content_type_id", "role_field"), + ("descendent", "content_type_id", "role_field"), + ] + + descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') + ancestor = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') + role_field = models.TextField(null=False) + #content_type_id = models.PositiveIntegerField(null=False) + #object_id = models.PositiveIntegerField(null=False) + content_type_id = models.PositiveIntegerField(null=False) + object_id = models.PositiveIntegerField(null=False) def get_roles_on_resource(resource, accessor): @@ -371,15 +383,18 @@ def get_roles_on_resource(resource, accessor): if type(accessor) == User: roles = accessor.roles.all() elif type(accessor) == Role: - roles = accessor + roles = [accessor] else: accessor_type = ContentType.objects.get_for_model(accessor) roles = Role.objects.filter(content_type__pk=accessor_type.id, object_id=accessor.id) - return { role.role_field: True for role in - Role.objects.filter( - content_type = ContentType.objects.get_for_model(resource), - object_id = resource.id, - ancestors = roles)} + return { + role_field: True for role_field in + RoleAncestorEntry.objects.filter( + ancestor__in=roles, + content_type_id=ContentType.objects.get_for_model(resource).id, + object_id=resource.id + ).values_list('role_field', flat=True) + } From 850e4c3ace6aa705f5b7a346d23a783905d54cef Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:30:06 -0400 Subject: [PATCH 36/56] Test fixes and a couple of view optimizations --- awx/api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index fdde047b33..ee36c41a11 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -667,7 +667,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView): org_id = int(self.kwargs['pk']) org_counts = {} - access_kwargs = {'accessor': self.request.user, 'role_name': 'read_role'} + access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'} direct_counts = Organization.objects.filter(id=org_id).annotate( users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True) @@ -784,8 +784,8 @@ class TeamList(ListCreateAPIView): serializer_class = TeamSerializer def get_queryset(self): - qs = Team.accessible_objects(self.request.user, 'read_role') - qs = qs.select_related('admin_role', 'auditor_role', 'member_role') + qs = Team.accessible_objects(self.request.user, 'read_role').order_by() + qs = qs.select_related('admin_role', 'auditor_role', 'member_role', 'organization') return qs class TeamDetail(RetrieveUpdateDestroyAPIView): @@ -1263,7 +1263,7 @@ class TeamCredentialsList(CredentialList): raise PermissionDenied() visible_creds = Credential.accessible_objects(self.request.user, 'read_role') - team_creds = Credential.objects.filter(owner_role__parents=team.member_role).distinct() + team_creds = Credential.objects.filter(owner_role__parents=team.member_role) return team_creds & visible_creds def post(self, request, *args, **kwargs): From 849a9c08b0d37a04829765ace06b393df23a074c Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:30:30 -0400 Subject: [PATCH 37/56] Drop our implicit order by inventory for Host models Sorting by inventory (which is really by inventory.name) which is untenable for a large number of hosts --- awx/main/models/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 963fe5b8da..c42bb77967 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -343,7 +343,7 @@ class Host(CommonModelNameNotUnique, ResourceMixin): class Meta: app_label = 'main' unique_together = (("name", "inventory"),) # FIXME: Add ('instance_id', 'inventory') after migration. - ordering = ('inventory', 'name') + ordering = ('name',) inventory = models.ForeignKey( 'Inventory', From a7f9eedb2118b3f5002c41ae2fd6b51c19456fd7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:32:34 -0400 Subject: [PATCH 38/56] Be more explicit with our user_admin_role selection to avoid potential future bugs --- awx/main/access.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index c4c8f05517..1c49bb7e4d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -63,7 +63,10 @@ def register_access(model_class, access_class): @property def user_admin_role(self): - return Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=self.id) + return Role.objects.get( + content_type=ContentType.objects.get_for_model(User), + object_id=self.id, + role_field='admin_role') def user_accessible_objects(user, role_name): return ResourceMixin._accessible_objects(User, user, role_name) From 74caa18cb5dc20b7d6e03b7823c36bebf23db4c2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:32:54 -0400 Subject: [PATCH 39/56] Ditch some expensive prefetching for Hosts This prefetching gets really expensive when we have a large number of hosts, it's far less expensive to just go out and do subsequent queries to pull this data in. (dropped aggregate query time down from ~2400ms to ~180ms) --- awx/main/access.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 1c49bb7e4d..10df3b4e63 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -365,10 +365,11 @@ class HostAccess(BaseAccess): inv_qs = Inventory.accessible_objects(self.user, 'read_role') group_qs = Group.accessible_objects(self.user, 'read_role') qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct() - qs = qs.select_related('created_by', 'modified_by', 'inventory', - 'last_job__job_template', - 'last_job_host_summary__job') - return qs.prefetch_related('groups').all() + #qs = qs.select_related('created_by', 'modified_by', 'inventory', + # 'last_job__job_template', + # 'last_job_host_summary__job') + #return qs.prefetch_related('groups').all() + return qs def can_read(self, obj): return obj and any(self.user in grp.read_role for grp in obj.groups.all()) or self.user in obj.inventory.read_role From a9f2507e917c9469daa6e8feb866b059fa690bd5 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 16 Apr 2016 18:54:45 -0400 Subject: [PATCH 40/56] flake8 --- awx/main/access.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 10df3b4e63..e9fd2760b7 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -64,9 +64,10 @@ def register_access(model_class, access_class): @property def user_admin_role(self): return Role.objects.get( - content_type=ContentType.objects.get_for_model(User), - object_id=self.id, - role_field='admin_role') + content_type=ContentType.objects.get_for_model(User), + object_id=self.id, + role_field='admin_role' + ) def user_accessible_objects(user, role_name): return ResourceMixin._accessible_objects(User, user, role_name) From 85843cc6ad2d1e893cd7f6c52fd1abb0ab89636d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 17 Apr 2016 10:20:38 -0400 Subject: [PATCH 41/56] Fixed up some RBAC indexing --- awx/main/migrations/0008_v300_rbac_changes.py | 2 +- awx/main/models/rbac.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 79684e9a5f..52c8bcfc3c 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -97,7 +97,7 @@ class Migration(migrations.Migration): ), migrations.AlterIndexTogether( name='roleancestorentry', - index_together=set([('descendent', 'content_type_id', 'role_field'), ('ancestor', 'content_type_id', 'role_field')]), + index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field')]), ), migrations.AddField( diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index d09d7d2dda..39c73cbe2f 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -360,8 +360,8 @@ class RoleAncestorEntry(models.Model): verbose_name_plural = _('role_ancestors') db_table = 'main_rbac_role_ancestors' index_together = [ - ("ancestor", "content_type_id", "role_field"), - ("descendent", "content_type_id", "role_field"), + ("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource + ("ancestor", "content_type_id", "role_field"), # used by accessible_objects ] descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') From 2a676d80ce01613d36f356ff204d3fdb9a79bffb Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 08:45:44 -0400 Subject: [PATCH 42/56] Handle vacuous role rebuilding condition instead of exploding --- awx/main/models/rbac.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 39c73cbe2f..79a13eb485 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -234,6 +234,9 @@ class Role(CommonModelNameNotUnique): # # + if len(role_ids_to_rebuild) == 0: + return + cursor = connection.cursor() loop_ct = 0 From 6d8bab97dfe89cf3854c6a5da941d7d8b19ed190 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 08:53:40 -0400 Subject: [PATCH 43/56] Only rebuild ancestor list on post_* events --- awx/main/signals.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index a6767177b0..e4893d34c2 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -106,13 +106,14 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs): if inventory is not None: update_inventory_computed_fields.delay(inventory.id, True) -def rebuild_role_ancestor_list(reverse, model, instance, pk_set, **kwargs): +def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwargs): 'When a role parent is added or removed, update our role hierarchy list' - if reverse: - for id in pk_set: - model.objects.get(id=id).rebuild_role_ancestor_list() - else: - instance.rebuild_role_ancestor_list() + if action in ['post_add', 'post_remove', 'post_clear']: + if reverse: + for id in pk_set: + model.objects.get(id=id).rebuild_role_ancestor_list() + else: + instance.rebuild_role_ancestor_list() def sync_superuser_status_to_rbac(instance, **kwargs): 'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role' From fb013ed7f11205e558974d6f4a4c129e75dd2e1b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 18 Apr 2016 11:15:05 -0400 Subject: [PATCH 44/56] org counts: un-skip tests, more thorough counting --- .../api/test_organization_counts.py | 145 ++++++++++-------- 1 file changed, 83 insertions(+), 62 deletions(-) diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 550068da63..5aaef06b3d 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -3,72 +3,118 @@ import pytest from django.core.urlresolvers import reverse @pytest.fixture -def resourced_organization(organization, project, team, inventory, user): - admin_user = user('test-admin', True) - member_user = user('org-member') +def organization_resource_creator(organization, user): + def rf(users, admins, job_templates, projects, inventories, teams): - # Associate one resource of every type with the organization - organization.member_role.members.add(member_user) - organization.admin_role.members.add(admin_user) - # organization.teams.create(name='org-team') - # inventory = organization.inventories.create(name="associated-inv") - project.jobtemplates.create(name="test-jt", - description="test-job-template-desc", - inventory=inventory, - playbook="test_playbook.yml") + # Associate one resource of every type with the organization + for i in range(users): + member_user = user('org-member %s' % i) + organization.member_role.members.add(member_user) + for i in range(admins): + admin_user = user('org-admin %s' % i) + organization.admin_role.members.add(admin_user) + for i in range(teams): + organization.teams.create(name='org-team %s' % i) + for i in range(inventories): + inventory = organization.inventories.create(name="associated-inv %s" % i) + for i in range(projects): + organization.projects.create(name="test-proj %s" % i, + description="test-proj-desc", + scm_type="git", + scm_url="https://github.com/jlaska/ansible-playbooks") + # Mix up the inventories and projects used by the job templates + i_proj = 0 + i_inv = 0 + for i in range(job_templates): + project = organization.projects.all()[i_proj] + inventory = organization.inventories.all()[i_inv] + project.jobtemplates.create(name="test-jt %s" % i, + description="test-job-template-desc", + inventory=inventory, + playbook="test_playbook.yml") + i_proj += 1 + i_inv += 1 + if i_proj >= organization.projects.count(): + i_proj = 0 + if i_inv >= organization.inventories.count(): + i_inv = 0 - return organization + return organization + return rf +COUNTS_PRIMES = { + 'users': 11, + 'admins': 5, + 'job_templates': 3, + 'projects': 3, + 'inventories': 7, + 'teams': 5 +} +COUNTS_ZEROS = { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 +} + +@pytest.fixture +def resourced_organization(organization_resource_creator): + return organization_resource_creator(**COUNTS_PRIMES) @pytest.mark.django_db -def test_org_counts_detail_view(resourced_organization, user, get): +def test_org_counts_detail_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser external_admin = user('admin', True) response = get(reverse('api:organization_detail', args=[resourced_organization.pk]), external_admin) assert response.status_code == 200 + counts = response.data['summary_fields']['related_field_counts'] + assert counts == COUNTS_PRIMES + +@pytest.mark.django_db +def test_org_counts_detail_member(resourced_organization, user, get): + # Check that a non-admin org member can only see users / admin in detail view + member_user = resourced_organization.member_role.members.get(username='org-member 1') + response = get(reverse('api:organization_detail', + args=[resourced_organization.pk]), member_user) + assert response.status_code == 200 + counts = response.data['summary_fields']['related_field_counts'] assert counts == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 + 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins + 'admins': COUNTS_PRIMES['admins'], + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 } @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented -def test_org_counts_admin(resourced_organization, user, get): +def test_org_counts_list_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser external_admin = user('admin', True) response = get(reverse('api:organization_list', args=[]), external_admin) assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] - assert counts == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 - } + assert counts == COUNTS_PRIMES @pytest.mark.django_db -def test_org_counts_member(resourced_organization, get): +def test_org_counts_list_member(resourced_organization, user, get): # Check that a non-admin user can only see the full project and # user count, consistent with the RBAC rules - member_user = resourced_organization.member_role.members.get(username='org-member') + member_user = resourced_organization.member_role.members.get(username='org-member 1') response = get(reverse('api:organization_list', args=[]), member_user) assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] assert counts == { - 'users': 1, # Policy is that members can see other users and admins - 'admins': 1, + 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins + 'admins': COUNTS_PRIMES['admins'], 'job_templates': 0, 'projects': 0, 'inventories': 0, @@ -86,17 +132,9 @@ def test_new_org_zero_counts(user, post): new_org_list = post_response.render().data counts_dict = new_org_list['summary_fields']['related_field_counts'] - assert counts_dict == { - 'users': 0, - 'admins': 0, - 'job_templates': 0, - 'projects': 0, - 'inventories': 0, - 'teams': 0 - } + assert counts_dict == COUNTS_ZEROS @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented def test_two_organizations(resourced_organization, organizations, user, get): # Check correct results for two organizations are returned external_admin = user('admin', True) @@ -111,26 +149,10 @@ def test_two_organizations(resourced_organization, organizations, user, get): org_id = response.data['results'][i]['id'] counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] - assert counts[org_id_full] == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 - } - assert counts[org_id_zero] == { - 'users': 0, - 'admins': 0, - 'job_templates': 0, - 'projects': 0, - 'inventories': 0, - 'teams': 0 - } + assert counts[org_id_full] == COUNTS_PRIMES + assert counts[org_id_zero] == COUNTS_ZEROS -@pytest.mark.skip(reason="resolution planned for after RBAC merge") @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented def test_JT_associated_with_project(organizations, project, user, get): # Check that adding a project to an organization gets the project's JT # included in the organization's JT count @@ -163,4 +185,3 @@ def test_JT_associated_with_project(organizations, project, user, get): 'inventories': 0, 'teams': 0 } - From 8b67f1d1c6d898a823ce2652b9cbca290f2b5e81 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 11:27:22 -0400 Subject: [PATCH 45/56] Removed team<->org role cycle --- awx/main/migrations/0008_v300_rbac_changes.py | 4 ++-- awx/main/models/organization.py | 3 +-- awx/main/tests/functional/test_teams.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 awx/main/tests/functional/test_teams.py diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 52c8bcfc3c..cb6a653890 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -238,7 +238,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='team', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', parent_role=b'admin_role', to='main.Role', role_name=b'Team Member', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', to='main.Role', role_name=b'Team Member', null=b'True'), ), migrations.AddField( @@ -294,7 +294,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='team', name='read_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'), ), migrations.AlterField( model_name='credential', diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 9ef93e0b98..571f9117ab 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -122,12 +122,11 @@ class Team(CommonModelNameNotUnique, ResourceMixin): member_role = ImplicitRoleField( role_name='Team Member', role_description='A member of this team', - parent_role='admin_role', ) read_role = ImplicitRoleField( role_name='Read', role_description='Can view this team', - parent_role=['auditor_role', 'member_role'], + parent_role=['admin_role', 'auditor_role', 'member_role'], ) def get_absolute_url(self): diff --git a/awx/main/tests/functional/test_teams.py b/awx/main/tests/functional/test_teams.py new file mode 100644 index 0000000000..f1037f0462 --- /dev/null +++ b/awx/main/tests/functional/test_teams.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.mark.django_db() +def test_admin_not_member(team): + "Test to ensure we don't add admin_role as a parent to team.member_role, as " + "this creates a cycle with organization administration, which we've decided " + "to remove support for" + + assert team.admin_role.is_ancestor_of(team.member_role) is False From 878455187f73b370b0585ce19568e5f0663539dc Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 11:28:46 -0400 Subject: [PATCH 46/56] Optimized viewable user list, fixed up some project readability bugs --- awx/main/access.py | 21 ++++++++++++------- awx/main/migrations/0008_v300_rbac_changes.py | 2 +- awx/main/models/projects.py | 11 +++++----- awx/main/tests/functional/test_projects.py | 4 +++- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index e9fd2760b7..7b28b89ae8 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -212,11 +212,18 @@ class UserAccess(BaseAccess): if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and self.user.admin_of_organizations.exists(): return User.objects.all() - viewable_users_set = set() - viewable_users_set.update(self.user.roles.values_list('ancestors__members__id', flat=True)) - viewable_users_set.update(self.user.roles.values_list('descendents__members__id', flat=True)) + return ( + User.objects.filter( + pk__in=Organization.accessible_objects(self.user, 'read_role').values('member_role__members') + ) | + User.objects.filter( + pk=self.user.id + ) | + User.objects.filter( + pk__in=Role.objects.filter(singleton_name__in = [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]).values('members') + ) + ).distinct() - return User.objects.filter(id__in=viewable_users_set) def can_add(self, data): if data is not None and 'is_superuser' in data: @@ -576,11 +583,11 @@ class TeamAccess(BaseAccess): ''' I can see a team when: - I'm a superuser. - - I'm an admin of the team's organization. + - I'm an admin of the team - I'm a member of that team. I can create/change a team when: - I'm a superuser. - - I'm an org admin for the team's org. + - I'm an admin for the team ''' model = Team @@ -604,7 +611,7 @@ class TeamAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: raise PermissionDenied('Unable to change organization on a team') - return self.user in obj.organization.admin_role + return self.user in obj.admin_role def can_delete(self, obj): return self.can_change(obj, None) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index cb6a653890..4b87865c5f 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -284,7 +284,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='project', name='read_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=b'member_role', to='main.Role', role_name=b'Project Read Access', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'auditor_role', b'scm_update_role', b'member_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'), ), migrations.AddField( model_name='role', diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 097a714fdf..cdc4d17320 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -241,17 +241,16 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): role_description='Implies membership within this project', parent_role='admin_role', ) - read_role = ImplicitRoleField( - role_name='Project Read Access', - role_description='Read access to this project', - parent_role='member_role', - ) - scm_update_role = ImplicitRoleField( role_name='Project Updater', role_description='May update this project from the source control management system', parent_role='admin_role', ) + read_role = ImplicitRoleField( + role_name='Project Read Access', + role_description='Read access to this project', + parent_role=['member_role', 'auditor_role', 'scm_update_role'], + ) @classmethod def _get_unified_job_class(cls): diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index cfed6ddfcf..aaf0802791 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -11,9 +11,11 @@ from awx.main.models import Project # @pytest.mark.django_db(transaction=True) -def test_user_project_list(get, project_factory, admin, alice, bob): +def test_user_project_list(get, project_factory, organization, admin, alice, bob): 'List of projects a user has access to, filtered by projects you can also see' + organization.member_role.members.add(alice, bob) + alice_project = project_factory('alice project') alice_project.admin_role.members.add(alice) From 5abde762ae0f6ba9a6c17dd0e375bdbbf9bb5a51 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 18 Apr 2016 14:29:30 -0400 Subject: [PATCH 47/56] updates to prompt-for tests and logic for new RBAC updates --- awx/api/views.py | 4 ++-- .../functional/api/test_job_runtime_params.py | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 80edcad4a5..05d908f8e4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2120,12 +2120,12 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None): new_credential = Credential.objects.get(pk=prompted_fields['credential']) - if not request.user.can_access(Credential, 'use', new_credential): + if request.user not in new_credential.use_role: raise PermissionDenied() if 'inventory' in prompted_fields and prompted_fields['inventory'] != getattrd(obj, 'inventory.pk', None): new_inventory = Inventory.objects.get(pk=prompted_fields['inventory']) - if not request.user.can_access(Inventory, 'use', new_inventory): + if request.user not in new_inventory.use_role: raise PermissionDenied() kv = prompted_fields diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index cc417111db..28bc250d8f 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -3,6 +3,7 @@ import yaml from awx.api.serializers import JobLaunchSerializer from awx.main.models.credential import Credential +from awx.main.models.inventory import Inventory from awx.main.models.jobs import Job, JobTemplate from django.core.urlresolvers import reverse @@ -93,7 +94,7 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user job_template = job_template_prompts(True) admin_user = user('admin', True) - job_template.inventory.executor_role.members.add(admin_user) + job_template.inventory.execute_role.members.add(admin_user) job_template.inventory.save() response = post(reverse('api:job_template_launch', args=[job_template.pk]), @@ -112,20 +113,19 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user assert job_obj.job_tags == runtime_data['job_tags'] @pytest.mark.django_db -@pytest.mark.skip(reason="JT can_start without inventory needs to be fixed before passing") @pytest.mark.job_runtime_vars def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, user): job_template = job_template_prompts_null - common_user = user('admin', False) + common_user = user('not-admin', False) - job_template.executor_role.members.add(common_user) - job_template.save() - job_template.project.member_role.members.add(common_user) - job_template.project.save() + # Give user permission to execute the job template + job_template.execute_role.members.add(common_user) + # Give user permission to use inventory and credential at runtime credential = Credential.objects.get(pk=runtime_data['credential']) - credential.usage_role.members.add(common_user) - credential.save() + credential.use_role.members.add(common_user) + inventory = Inventory.objects.get(pk=runtime_data['inventory']) + inventory.use_role.members.add(common_user) response = post(reverse('api:job_template_launch', args=[job_template.pk]), runtime_data, common_user) @@ -187,13 +187,13 @@ def test_job_launch_fails_without_inventory_access(deploy_jobtemplate, machine_c deploy_jobtemplate.ask_inventory_on_launch = True deploy_jobtemplate.credential = machine_credential common_user = user('test-user', False) - deploy_jobtemplate.executor_role.members.add(common_user) + deploy_jobtemplate.execute_role.members.add(common_user) deploy_jobtemplate.save() - deploy_jobtemplate.inventory.usage_role.members.add(common_user) + deploy_jobtemplate.inventory.use_role.members.add(common_user) deploy_jobtemplate.inventory.save() deploy_jobtemplate.project.member_role.members.add(common_user) deploy_jobtemplate.project.save() - deploy_jobtemplate.credential.usage_role.members.add(common_user) + deploy_jobtemplate.credential.use_role.members.add(common_user) deploy_jobtemplate.credential.save() # Assure that the base job template can be launched to begin with From 0cf096e5b07d425579b51a080b6f839072194679 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 14:31:42 -0400 Subject: [PATCH 48/56] Updated users.py test expectations to match our current behavior --- awx/main/tests/old/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/old/users.py b/awx/main/tests/old/users.py index e6e5b1ddba..de364ff161 100644 --- a/awx/main/tests/old/users.py +++ b/awx/main/tests/old/users.py @@ -420,10 +420,10 @@ class UsersTest(BaseTest): self.assertEquals(data1['count'], 3) # Normal user can no longer see all users after the organization he # admins is marked inactive, nor can he see any other users that were - # in that org, so he only sees himself. + # in that org, so he only sees himself and the system admin. self.organizations[0].delete() data3 = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(data3['count'], 1) + self.assertEquals(data3['count'], 2) # Test no longer relevant since we've moved away from active / inactive. # However there was talk about keeping is_active for users, so this test will From 03497375387788d51aefb9ec412bb07886c86574 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 14:32:21 -0400 Subject: [PATCH 49/56] Attempt at a workaround for our larger sqlite tests These tests are only failing on jenkins, not on our local dev environments. --- awx/main/models/rbac.py | 160 ++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 97 deletions(-) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 79a13eb485..23920eb75b 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey + # AWX from django.contrib.auth.models import User # noqa from awx.main.models.base import * # noqa @@ -58,7 +59,7 @@ def batch_role_ancestor_rebuilding(allow_nesting=False): if not batch_role_rebuilding: rebuild_set = getattr(tls, 'roles_needing_rebuilding') with transaction.atomic(): - Role._simultaneous_ancestry_rebuild(rebuild_set) + Role._simultaneous_ancestry_rebuild(list(rebuild_set)) #for role in Role.objects.filter(id__in=list(rebuild_set)).all(): # # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this @@ -128,28 +129,6 @@ class Role(CommonModelNameNotUnique): roles_needing_rebuilding.add(self.id) return - #actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True)) - #actual_ancestors.add(self.id) - #if None in actual_ancestors: - # actual_ancestors.remove(None) - #stored_ancestors = set(self.ancestors.all().values_list('id', flat=True)) - - ''' - # If it differs, update, and then update all of our children - if actual_ancestors != stored_ancestors: - for id in actual_ancestors - stored_ancestors: - self.ancestors.add(id) - for id in stored_ancestors - actual_ancestors: - self.ancestors.remove(id) - - for child in self.children.all(): - child.rebuild_role_ancestor_list() - ''' - - # If our role heirarchy hasn't actually changed, don't do anything - #if actual_ancestors == stored_ancestors: - # return - Role._simultaneous_ancestry_rebuild([self.id]) @@ -244,29 +223,19 @@ class Role(CommonModelNameNotUnique): 'ancestors_table': Role.ancestors.through._meta.db_table, 'parents_table': Role.parents.through._meta.db_table, 'roles_table': Role._meta.db_table, - 'ids': ','.join(str(x) for x in role_ids_to_rebuild) } - # This is our solution for dealing with updates to parents of nodes - # that are in cycles. It seems like we should be able to find a more - # clever way of just dealing with the issue in another way, but it's - # surefire and I'm not seeing the easy solution to dealing with that - # problem that's not this. - # TODO: Test to see if not deleting any entry that has a direct - # correponding entry in the parents table helps reduce the processing - # time significantly - """ - cursor.execute(''' - DELETE FROM %(ancestors_table)s - WHERE ancestor_id IN (%(ids)s) - AND descendent_id != ancestor_id - ''' % sql_params) - """ - cursor.execute(''' - DELETE FROM %(ancestors_table)s - WHERE ancestor_id IN (%(ids)s) - ''' % sql_params) + def split_ids_for_sqlite(role_ids): + for i in xrange(0, len(role_ids), 999): + yield role_ids[i:i + 999] + + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE ancestor_id IN (%(ids)s) + ''' % sql_params) while role_ids_to_rebuild: @@ -274,66 +243,63 @@ class Role(CommonModelNameNotUnique): raise Exception('Ancestry role rebuilding error: infinite loop detected') loop_ct += 1 - sql_params = { - 'ancestors_table': Role.ancestors.through._meta.db_table, - 'parents_table': Role.parents.through._meta.db_table, - 'roles_table': Role._meta.db_table, - 'ids': ','.join(str(x) for x in role_ids_to_rebuild) - } - delete_ct = 0 + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE descendent_id IN (%(ids)s) + AND + id NOT IN ( + SELECT %(ancestors_table)s.id FROM ( + SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id + FROM %(parents_table)s as parents + LEFT JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL - cursor.execute(''' - DELETE FROM %(ancestors_table)s - WHERE descendent_id IN (%(ids)s) - AND - id NOT IN ( - SELECT %(ancestors_table)s.id FROM ( - SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id - FROM %(parents_table)s as parents - LEFT JOIN %(ancestors_table)s as ancestors - ON (parents.to_role_id = ancestors.descendent_id) - WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL + UNION - UNION + SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) + WHERE %(ancestors_table)s.id IS NOT NULL + ) + ''' % sql_params) + delete_ct += cursor.rowcount - SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) - ) new_ancestry_list - LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id - AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) - WHERE %(ancestors_table)s.id IS NOT NULL - ) - ''' % sql_params) - delete_ct = cursor.rowcount + insert_ct = 0 + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id) + SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM ( + SELECT parents.from_role_id from_id, + ancestors.ancestor_id to_id, + roles.role_field, + COALESCE(roles.content_type_id, 0) content_type_id, + COALESCE(roles.object_id, 0) object_id + FROM %(parents_table)s as parents + INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id) + LEFT OUTER JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL - cursor.execute(''' - INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id) - SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM ( - SELECT parents.from_role_id from_id, - ancestors.ancestor_id to_id, - roles.role_field, - COALESCE(roles.content_type_id, 0) content_type_id, - COALESCE(roles.object_id, 0) object_id - FROM %(parents_table)s as parents - INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id) - LEFT OUTER JOIN %(ancestors_table)s as ancestors - ON (parents.to_role_id = ancestors.descendent_id) - WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL + UNION - UNION - - SELECT id from_id, - id to_id, - role_field, - COALESCE(content_type_id, 0) content_type_id, - COALESCE(object_id, 0) object_id - from %(roles_table)s WHERE id IN (%(ids)s) - ) new_ancestry_list - LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id - AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) - WHERE %(ancestors_table)s.id IS NULL - ''' % sql_params) - insert_ct = cursor.rowcount + SELECT id from_id, + id to_id, + role_field, + COALESCE(content_type_id, 0) content_type_id, + COALESCE(object_id, 0) object_id + from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) + WHERE %(ancestors_table)s.id IS NULL + ''' % sql_params) + insert_ct += cursor.rowcount if insert_ct == 0 and delete_ct == 0: break From 1a97773aa1fcfd0b27b3d58860a4a77c83ad4008 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 18 Apr 2016 14:36:18 -0400 Subject: [PATCH 50/56] remove more save calls --- awx/main/tests/functional/api/test_job_runtime_params.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index 28bc250d8f..470e43423f 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -95,7 +95,6 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user admin_user = user('admin', True) job_template.inventory.execute_role.members.add(admin_user) - job_template.inventory.save() response = post(reverse('api:job_template_launch', args=[job_template.pk]), runtime_data, admin_user) @@ -186,15 +185,12 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user): def test_job_launch_fails_without_inventory_access(deploy_jobtemplate, machine_credential, post, user): deploy_jobtemplate.ask_inventory_on_launch = True deploy_jobtemplate.credential = machine_credential + deploy_jobtemplate.save() common_user = user('test-user', False) deploy_jobtemplate.execute_role.members.add(common_user) - deploy_jobtemplate.save() deploy_jobtemplate.inventory.use_role.members.add(common_user) - deploy_jobtemplate.inventory.save() deploy_jobtemplate.project.member_role.members.add(common_user) - deploy_jobtemplate.project.save() deploy_jobtemplate.credential.use_role.members.add(common_user) - deploy_jobtemplate.credential.save() # Assure that the base job template can be launched to begin with response = post(reverse('api:job_template_launch', From 96aa3e255505de1caa3e13fd5b139f413fc59962 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 15:26:09 -0400 Subject: [PATCH 51/56] Attempt 2 at making jenkins' sqlite happy --- awx/main/models/rbac.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 23920eb75b..6ca5ef4109 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -304,9 +304,14 @@ class Role(CommonModelNameNotUnique): if insert_ct == 0 and delete_ct == 0: break - role_ids_to_rebuild = Role.objects.distinct() \ - .filter(id__in=role_ids_to_rebuild, children__id__isnull=False) \ - .values_list('children__id', flat=True) + new_role_ids_to_rebuild = set() + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + new_role_ids_to_rebuild.update(set(Role.objects.distinct() + .filter(id__in=ids, children__id__isnull=False) + .values_list('children__id', flat=True))) + role_ids_to_rebuild = list(new_role_ids_to_rebuild) + From 07eccbe3e4eda7a203df09f713a551b9983b02ea Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 16:22:14 -0400 Subject: [PATCH 52/56] Migration for dropping implicit host inventory,name ordering --- awx/main/migrations/0018_v300_host_ordering.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 awx/main/migrations/0018_v300_host_ordering.py diff --git a/awx/main/migrations/0018_v300_host_ordering.py b/awx/main/migrations/0018_v300_host_ordering.py new file mode 100644 index 0000000000..0309875de6 --- /dev/null +++ b/awx/main/migrations/0018_v300_host_ordering.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0017_v300_prompting_migrations'), + ] + + operations = [ + migrations.AlterModelOptions( + name='host', + options={'ordering': ('name',)}, + ), + ] From c3144dc4d367cd6f9a107effa66427d767fbbcad Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 16:26:11 -0400 Subject: [PATCH 53/56] benign 0008 migration change to make makemigrations happy --- awx/main/migrations/0008_v300_rbac_changes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 4b87865c5f..0cd6abca3c 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -284,7 +284,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='project', name='read_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'auditor_role', b'scm_update_role', b'member_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'), ), migrations.AddField( model_name='role', From 41553c39bf4e28c1c40e89c45cd23f706e524395 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 16:56:29 -0400 Subject: [PATCH 54/56] Moved dummy data generator out to tools directory --- .../commands/generate_dummy_data.py | 354 --------------- .../rbac_dummy_data_generator.py | 402 ++++++++++++++++++ 2 files changed, 402 insertions(+), 354 deletions(-) delete mode 100644 awx/main/management/commands/generate_dummy_data.py create mode 100755 tools/data_generators/rbac_dummy_data_generator.py diff --git a/awx/main/management/commands/generate_dummy_data.py b/awx/main/management/commands/generate_dummy_data.py deleted file mode 100644 index 9f1b2cf83f..0000000000 --- a/awx/main/management/commands/generate_dummy_data.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright (c) 2016 Ansible, Inc. -# All Rights Reserved - -# Python -import sys -from collections import defaultdict -from optparse import make_option - - -# Django -from django.core.management.base import BaseCommand -from django.utils.timezone import now -from django.contrib.auth.models import User -from django.db import transaction - -# awx -from awx.main.models import * # noqa - - - -class Rollback(Exception): - pass - -class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--organizations', action='store', type='int', default=3, - help='Number of organizations to create'), - make_option('--users', action='store', type='int', default=10, - help='Number of users to create'), - make_option('--teams', action='store', type='int', default=5, - help='Number of teams to create'), - make_option('--projects', action='store', type='int', default=10, - help='Number of projects to create'), - make_option('--job-templates', action='store', type='int', default=20, - help='Number of job templates to create'), - make_option('--credentials', action='store', type='int', default=5, - help='Number of credentials to create'), - make_option('--inventories', action='store', type='int', default=5, - help='Number of credentials to create'), - make_option('--inventory-groups', action='store', type='int', default=10, - help='Number of credentials to create'), - make_option('--inventory-hosts', action='store', type='int', default=40, - help='number of credentials to create'), - make_option('--jobs', action='store', type='int', default=200, - help='number of job entries to create'), - make_option('--job-events', action='store', type='int', default=500, - help='number of job event entries to create'), - make_option('--pretend', action='store_true', - help="Don't commit the data to the database"), - make_option('--prefix', action='store', type='string', default='', - help="Prefix generated names with this string"), - #make_option('--spread-bias', action='store', type='string', default='exponential', - # help='"exponential" to bias associations exponentially front loaded for - for ex'), - ) - - def handle(self, *args, **options): - n_organizations = int(options['organizations']) - n_users = int(options['users']) - n_teams = int(options['teams']) - n_projects = int(options['projects']) - n_job_templates = int(options['job_templates']) - n_credentials = int(options['credentials']) - n_inventories = int(options['inventories']) - n_inventory_groups = int(options['inventory_groups']) - n_inventory_hosts = int(options['inventory_hosts']) - n_jobs = int(options['jobs']) - n_job_events = int(options['job_events']) - prefix = options['prefix'] - - organizations = [] - users = [] - teams = [] - projects = [] - job_templates = [] - credentials = [] - inventories = [] - inventory_groups = [] - inventory_hosts = [] - jobs = [] - #job_events = [] - - def spread(n, m): - ret = [] - # At least one in each slot, split up the rest exponentially so the first - # buckets contain a lot of entries - for i in xrange(m): - if n > 0: - ret.append(1) - n -= 1 - else: - ret.append(0) - - for i in xrange(m): - n_in_this_slot = n // 2 - n-= n_in_this_slot - ret[i] += n_in_this_slot - if n > 0 and len(ret): - ret[0] += n - return ret - - ids = defaultdict(lambda: 0) - - - try: - - with transaction.atomic(): - with batch_role_ancestor_rebuilding(): - - print('# Creating %d organizations' % n_organizations) - for i in xrange(n_organizations): - sys.stdout.write('\r%d ' % (i + 1)) - sys.stdout.flush() - organizations.append(Organization.objects.create(name='%s Organization %d' % (prefix, i))) - print('') - - print('# Creating %d users' % n_users) - org_idx = 0 - for n in spread(n_users, n_organizations): - for i in range(n): - ids['user'] += 1 - user_id = ids['user'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, organizations[org_idx].name, i+ 1)) - sys.stdout.flush() - user = User.objects.create(username='%suser-%d' % (prefix, user_id)) - organizations[org_idx].member_role.members.add(user) - users.append(user) - org_idx += 1 - print('') - - print('# Creating %d teams' % n_teams) - org_idx = 0 - for n in spread(n_teams, n_organizations): - org = organizations[org_idx] - for i in range(n): - ids['team'] += 1 - team_id = ids['team'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - team = Team.objects.create(name='%s Team %d Org %d' % (prefix, team_id, org_idx), organization=org) - teams.append(team) - org_idx += 1 - print('') - - print('# Adding users to teams') - for org in organizations: - org_teams = [t for t in org.teams.all()] - org_users = [u for u in org.member_role.members.all()] - print(' Spreading %d users accross %d teams for %s' % (len(org_users), len(org_teams), org.name)) - # Our normal spread for most users - cur_user_idx = 0 - cur_team_idx = 0 - for n in spread(len(org_users), len(org_teams)): - team = org_teams[cur_team_idx] - for i in range(n): - if cur_user_idx < len(org_users): - user = org_users[cur_user_idx] - team.member_role.members.add(user) - cur_user_idx += 1 - cur_team_idx += 1 - - # First user gets added to all teams - for team in org_teams: - team.member_role.members.add(org_users[0]) - - - print('# Creating %d credentials for users' % (n_credentials - n_credentials // 2)) - user_idx = 0 - for n in spread(n_credentials - n_credentials // 2, n_users): - user = users[user_idx] - for i in range(n): - ids['credential'] += 1 - sys.stdout.write('\r %d ' % (ids['credential'])) - sys.stdout.flush() - credential_id = ids['credential'] - credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx)) - credential.owner_role.members.add(user) - credentials.append(credential) - user_idx += 1 - print('') - - print('# Creating %d credentials for teams' % (n_credentials // 2)) - team_idx = 0 - starting_credential_id = ids['credential'] - for n in spread(n_credentials - n_credentials // 2, n_teams): - team = teams[team_idx] - for i in range(n): - ids['credential'] += 1 - sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id)) - sys.stdout.flush() - credential_id = ids['credential'] - credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx)) - credential.owner_role.parents.add(team.member_role) - credentials.append(credential) - team_idx += 1 - print('') - - print('# Creating %d projects' % n_projects) - org_idx = 0 - for n in spread(n_projects, n_organizations): - org = organizations[org_idx] - for i in range(n): - ids['project'] += 1 - project_id = ids['project'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - project = Project.objects.create(name='%s Project %d Org %d' % (prefix, project_id, org_idx), organization=org) - projects.append(project) - - org_idx += 1 - print('') - - - print('# Creating %d inventories' % n_inventories) - org_idx = 0 - for n in spread(n_inventories, min(n_inventories // 4 + 1, n_organizations)): - org = organizations[org_idx] - for i in range(n): - ids['inventory'] += 1 - inventory_id = ids['inventory'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - inventory = Inventory.objects.create(name='%s Inventory %d Org %d' % (prefix, inventory_id, org_idx), organization=org) - inventories.append(inventory) - - org_idx += 1 - print('') - - - print('# Creating %d inventory_groups' % n_inventory_groups) - inv_idx = 0 - for n in spread(n_inventory_groups, n_inventories): - inventory = inventories[inv_idx] - parent_list = [None] * 3 - for i in range(n): - ids['group'] += 1 - group_id = ids['group'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, inventory.name, i+ 1)) - sys.stdout.flush() - group = Group.objects.create( - name='%s Group %d Inventory %d' % (prefix, group_id, inv_idx), - inventory=inventory, - ) - # Have each group have up to 3 parent groups - for parent_n in range(3): - if i // 4 + parent_n < len(parent_list) and parent_list[i // 4 + parent_n]: - group.parents.add(parent_list[i // 4 + parent_n]) - if parent_list[i // 4] is None: - parent_list[i // 4] = group - else: - parent_list.append(group) - inventory_groups.append(group) - - inv_idx += 1 - print('') - - - print('# Creating %d inventory_hosts' % n_inventory_hosts) - group_idx = 0 - for n in spread(n_inventory_hosts, n_inventory_groups): - group = inventory_groups[group_idx] - for i in range(n): - ids['host'] += 1 - host_id = ids['host'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, group.name, i+ 1)) - sys.stdout.flush() - host = Host.objects.create(name='%s Host %d Group %d' % (prefix, host_id, group_idx), inventory=group.inventory) - # Add the host to up to 3 groups - host.groups.add(group) - for m in range(2): - if group_idx + m < len(inventory_groups) and group.inventory.id == inventory_groups[group_idx + m].inventory.id: - host.groups.add(inventory_groups[group_idx + m]) - - inventory_hosts.append(host) - - group_idx += 1 - print('') - - print('# Creating %d job_templates' % n_job_templates) - project_idx = 0 - inv_idx = 0 - for n in spread(n_job_templates, n_projects): - project = projects[project_idx] - for i in range(n): - ids['job_template'] += 1 - job_template_id = ids['job_template'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, project.name, i+ 1)) - sys.stdout.flush() - - inventory = None - org_inv_count = project.organization.inventories.count() - if org_inv_count > 0: - inventory = project.organization.inventories.all()[inv_idx % org_inv_count] - - job_template = JobTemplate.objects.create( - name='%s Job Template %d Project %d' % (prefix, job_template_id, project_idx), - inventory=inventory, - project=project, - ) - job_templates.append(job_template) - inv_idx += 1 - project_idx += 1 - print('') - - print('# Creating %d jobs' % n_jobs) - group_idx = 0 - job_template_idx = 0 - for n in spread(n_jobs, n_job_templates): - job_template = job_templates[job_template_idx] - for i in range(n): - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, job_template.name, i+ 1)) - sys.stdout.flush() - job = Job.objects.create(job_template=job_template) - jobs.append(job) - - if job_template.inventory: - inv_groups = [g for g in job_template.inventory.groups.all()] - if len(inv_groups): - JobHostSummary.objects.bulk_create([ - JobHostSummary( - job=job, host=h, host_name=h.name, processed=1, - created=now(), modified=now() - ) - for h in inv_groups[group_idx % len(inv_groups)].hosts.all()[:100] - ]) - group_idx += 1 - job_template_idx += 1 - if n: - print('') - - print('# Creating %d job events' % n_job_events) - job_idx = 0 - for n in spread(n_job_events, n_jobs): - job = jobs[job_idx] - sys.stdout.write('\r Creating %d job events for job %d' % (n, job.id)) - sys.stdout.flush() - JobEvent.objects.bulk_create([ - JobEvent( - created=now(), - modified=now(), - job=job, - event='runner_on_ok' - ) - for i in range(n) - ]) - job_idx += 1 - if n: - print('') - - if options['pretend']: - raise Rollback() - except Rollback: - print('Rolled back changes') - pass - return diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py new file mode 100755 index 0000000000..5a0df82da5 --- /dev/null +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved +import os +import sys +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings.development") # noqa + +import django +django.setup() # noqa + + +# Python +from collections import defaultdict +from optparse import make_option, OptionParser + + +# Django + +from django.utils.timezone import now +from django.contrib.auth.models import User +from django.db import transaction + +# awx +from awx.main.models import * # noqa + + + + +option_list = [ + make_option('--organizations', action='store', type='int', default=3, + help='Number of organizations to create'), + make_option('--users', action='store', type='int', default=10, + help='Number of users to create'), + make_option('--teams', action='store', type='int', default=5, + help='Number of teams to create'), + make_option('--projects', action='store', type='int', default=10, + help='Number of projects to create'), + make_option('--job-templates', action='store', type='int', default=20, + help='Number of job templates to create'), + make_option('--credentials', action='store', type='int', default=5, + help='Number of credentials to create'), + make_option('--inventories', action='store', type='int', default=5, + help='Number of credentials to create'), + make_option('--inventory-groups', action='store', type='int', default=10, + help='Number of credentials to create'), + make_option('--inventory-hosts', action='store', type='int', default=40, + help='number of credentials to create'), + make_option('--jobs', action='store', type='int', default=200, + help='number of job entries to create'), + make_option('--job-events', action='store', type='int', default=500, + help='number of job event entries to create'), + make_option('--pretend', action='store_true', + help="Don't commit the data to the database"), + make_option('--prefix', action='store', type='string', default='', + help="Prefix generated names with this string"), + #make_option('--spread-bias', action='store', type='string', default='exponential', + # help='"exponential" to bias associations exponentially front loaded for - for ex'), +] +parser = OptionParser(option_list=option_list) +options, remainder = parser.parse_args() +options = vars(options) + + +n_organizations = int(options['organizations']) +n_users = int(options['users']) +n_teams = int(options['teams']) +n_projects = int(options['projects']) +n_job_templates = int(options['job_templates']) +n_credentials = int(options['credentials']) +n_inventories = int(options['inventories']) +n_inventory_groups = int(options['inventory_groups']) +n_inventory_hosts = int(options['inventory_hosts']) +n_jobs = int(options['jobs']) +n_job_events = int(options['job_events']) +prefix = options['prefix'] + +organizations = [] +users = [] +teams = [] +projects = [] +job_templates = [] +credentials = [] +inventories = [] +inventory_groups = [] +inventory_hosts = [] +jobs = [] +#job_events = [] + +def spread(n, m): + ret = [] + # At least one in each slot, split up the rest exponentially so the first + # buckets contain a lot of entries + for i in xrange(m): + if n > 0: + ret.append(1) + n -= 1 + else: + ret.append(0) + + for i in xrange(m): + n_in_this_slot = n // 2 + n-= n_in_this_slot + ret[i] += n_in_this_slot + if n > 0 and len(ret): + ret[0] += n + return ret + +ids = defaultdict(lambda: 0) + + +class Rollback(Exception): + pass + + +try: + + with batch_role_ancestor_rebuilding(): + with transaction.atomic(): + admin, _ = User.objects.get_or_create(username = 'admin', is_superuser=True) + org_admin, _ = User.objects.get_or_create(username = 'org_admin') + org_member, _ = User.objects.get_or_create(username = 'org_member') + prj_admin, _ = User.objects.get_or_create(username = 'prj_admin') + jt_admin, _ = User.objects.get_or_create(username = 'jt_admin') + inv_admin, _ = User.objects.get_or_create(username = 'inv_admin') + + admin.is_superuser = True + admin.save() + admin.set_password('test') + admin.save() + org_admin.set_password('test') + org_admin.save() + org_member.set_password('test') + org_member.save() + prj_admin.set_password('test') + prj_admin.save() + jt_admin.set_password('test') + jt_admin.save() + inv_admin.set_password('test') + inv_admin.save() + + + + print('# Creating %d organizations' % n_organizations) + for i in xrange(n_organizations): + sys.stdout.write('\r%d ' % (i + 1)) + sys.stdout.flush() + org = Organization.objects.create(name='%s Organization %d' % (prefix, i)) + organizations.append(org) + if i == 0: + org.admin_role.members.add(org_admin) + org.member_role.members.add(org_admin) + org.member_role.members.add(org_member) + org.member_role.members.add(prj_admin) + org.member_role.members.add(jt_admin) + org.member_role.members.add(inv_admin) + + print('') + + print('# Creating %d users' % n_users) + org_idx = 0 + for n in spread(n_users, n_organizations): + for i in range(n): + ids['user'] += 1 + user_id = ids['user'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, organizations[org_idx].name, i+ 1)) + sys.stdout.flush() + user = User.objects.create(username='%suser-%d' % (prefix, user_id)) + organizations[org_idx].member_role.members.add(user) + users.append(user) + org_idx += 1 + print('') + + print('# Creating %d teams' % n_teams) + org_idx = 0 + for n in spread(n_teams, n_organizations): + org = organizations[org_idx] + for i in range(n): + ids['team'] += 1 + team_id = ids['team'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + team = Team.objects.create(name='%s Team %d Org %d' % (prefix, team_id, org_idx), organization=org) + teams.append(team) + org_idx += 1 + print('') + + print('# Adding users to teams') + for org in organizations: + org_teams = [t for t in org.teams.all()] + org_users = [u for u in org.member_role.members.all()] + print(' Spreading %d users accross %d teams for %s' % (len(org_users), len(org_teams), org.name)) + # Our normal spread for most users + cur_user_idx = 0 + cur_team_idx = 0 + for n in spread(len(org_users), len(org_teams)): + team = org_teams[cur_team_idx] + for i in range(n): + if cur_user_idx < len(org_users): + user = org_users[cur_user_idx] + team.member_role.members.add(user) + cur_user_idx += 1 + cur_team_idx += 1 + + # First user gets added to all teams + for team in org_teams: + team.member_role.members.add(org_users[0]) + + + print('# Creating %d credentials for users' % (n_credentials - n_credentials // 2)) + user_idx = 0 + for n in spread(n_credentials - n_credentials // 2, n_users): + user = users[user_idx] + for i in range(n): + ids['credential'] += 1 + sys.stdout.write('\r %d ' % (ids['credential'])) + sys.stdout.flush() + credential_id = ids['credential'] + credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx)) + credential.owner_role.members.add(user) + credentials.append(credential) + user_idx += 1 + print('') + + print('# Creating %d credentials for teams' % (n_credentials // 2)) + team_idx = 0 + starting_credential_id = ids['credential'] + for n in spread(n_credentials - n_credentials // 2, n_teams): + team = teams[team_idx] + for i in range(n): + ids['credential'] += 1 + sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id)) + sys.stdout.flush() + credential_id = ids['credential'] + credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx)) + credential.owner_role.parents.add(team.member_role) + credentials.append(credential) + team_idx += 1 + print('') + + print('# Creating %d projects' % n_projects) + org_idx = 0 + for n in spread(n_projects, n_organizations): + org = organizations[org_idx] + for i in range(n): + ids['project'] += 1 + project_id = ids['project'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + project = Project.objects.create(name='%s Project %d Org %d' % (prefix, project_id, org_idx), organization=org) + projects.append(project) + if org_idx == 0 and i == 0: + project.admin_role.members.add(prj_admin) + + org_idx += 1 + print('') + + + print('# Creating %d inventories' % n_inventories) + org_idx = 0 + for n in spread(n_inventories, min(n_inventories // 4 + 1, n_organizations)): + org = organizations[org_idx] + for i in range(n): + ids['inventory'] += 1 + inventory_id = ids['inventory'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + inventory = Inventory.objects.create(name='%s Inventory %d Org %d' % (prefix, inventory_id, org_idx), organization=org) + inventories.append(inventory) + if org_idx == 0 and i == 0: + inventory.admin_role.members.add(inv_admin) + + org_idx += 1 + print('') + + + print('# Creating %d inventory_groups' % n_inventory_groups) + inv_idx = 0 + for n in spread(n_inventory_groups, n_inventories): + inventory = inventories[inv_idx] + parent_list = [None] * 3 + for i in range(n): + ids['group'] += 1 + group_id = ids['group'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, inventory.name, i+ 1)) + sys.stdout.flush() + group = Group.objects.create( + name='%s Group %d Inventory %d' % (prefix, group_id, inv_idx), + inventory=inventory, + ) + # Have each group have up to 3 parent groups + for parent_n in range(3): + if i // 4 + parent_n < len(parent_list) and parent_list[i // 4 + parent_n]: + group.parents.add(parent_list[i // 4 + parent_n]) + if parent_list[i // 4] is None: + parent_list[i // 4] = group + else: + parent_list.append(group) + inventory_groups.append(group) + + inv_idx += 1 + print('') + + + print('# Creating %d inventory_hosts' % n_inventory_hosts) + group_idx = 0 + for n in spread(n_inventory_hosts, n_inventory_groups): + group = inventory_groups[group_idx] + for i in range(n): + ids['host'] += 1 + host_id = ids['host'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, group.name, i+ 1)) + sys.stdout.flush() + host = Host.objects.create(name='%s.host-%06d.group-%05d.dummy' % (prefix, host_id, group_idx), inventory=group.inventory) + # Add the host to up to 3 groups + host.groups.add(group) + for m in range(2): + if group_idx + m < len(inventory_groups) and group.inventory.id == inventory_groups[group_idx + m].inventory.id: + host.groups.add(inventory_groups[group_idx + m]) + + inventory_hosts.append(host) + + group_idx += 1 + print('') + + print('# Creating %d job_templates' % n_job_templates) + project_idx = 0 + inv_idx = 0 + for n in spread(n_job_templates, n_projects): + project = projects[project_idx] + for i in range(n): + ids['job_template'] += 1 + job_template_id = ids['job_template'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, project.name, i+ 1)) + sys.stdout.flush() + + inventory = None + org_inv_count = project.organization.inventories.count() + if org_inv_count > 0: + inventory = project.organization.inventories.all()[inv_idx % org_inv_count] + + job_template = JobTemplate.objects.create( + name='%s Job Template %d Project %d' % (prefix, job_template_id, project_idx), + inventory=inventory, + project=project, + ) + job_templates.append(job_template) + inv_idx += 1 + if project_idx == 0 and i == 0: + job_template.admin_role.members.add(jt_admin) + project_idx += 1 + print('') + + print('# Creating %d jobs' % n_jobs) + group_idx = 0 + job_template_idx = 0 + for n in spread(n_jobs, n_job_templates): + job_template = job_templates[job_template_idx] + for i in range(n): + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, job_template.name, i+ 1)) + sys.stdout.flush() + job = Job.objects.create(job_template=job_template) + jobs.append(job) + + if job_template.inventory: + inv_groups = [g for g in job_template.inventory.groups.all()] + if len(inv_groups): + JobHostSummary.objects.bulk_create([ + JobHostSummary( + job=job, host=h, host_name=h.name, processed=1, + created=now(), modified=now() + ) + for h in inv_groups[group_idx % len(inv_groups)].hosts.all()[:100] + ]) + group_idx += 1 + job_template_idx += 1 + if n: + print('') + + print('# Creating %d job events' % n_job_events) + job_idx = 0 + for n in spread(n_job_events, n_jobs): + job = jobs[job_idx] + sys.stdout.write('\r Creating %d job events for job %d' % (n, job.id)) + sys.stdout.flush() + JobEvent.objects.bulk_create([ + JobEvent( + created=now(), + modified=now(), + job=job, + event='runner_on_ok' + ) + for i in range(n) + ]) + job_idx += 1 + if n: + print('') + + if options['pretend']: + raise Rollback() +except Rollback: + print('Rolled back changes') + pass From 043c71728c3c385a978128250d7434f7dbb02f02 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 17:06:47 -0400 Subject: [PATCH 55/56] Bump our monolithic test time back up.. will try and reduce it post-merge --- awx/main/tests/old/commands/commands_monolithic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/old/commands/commands_monolithic.py b/awx/main/tests/old/commands/commands_monolithic.py index ae384f044d..e0efcf5326 100644 --- a/awx/main/tests/old/commands/commands_monolithic.py +++ b/awx/main/tests/old/commands/commands_monolithic.py @@ -977,7 +977,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(new_inv.groups.count(), ngroups) self.assertEqual(new_inv.total_hosts, nhosts) self.assertEqual(new_inv.total_groups, ngroups) - self.assertElapsedLessThan(120) + self.assertElapsedLessThan(180) # TODO: We need to revisit this again to see if we can optimize this back to the sub-120 second range - anoek 2016-04-18 @unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False), 'Skip this test in local development environments, ' From ad423e976b6656bef6e3fe1879e431db9c3cd2e7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 18 Apr 2016 17:58:18 -0400 Subject: [PATCH 56/56] Bump splunk time limit up to pass tests, will fix in #1584 --- awx/main/tests/old/commands/commands_monolithic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/old/commands/commands_monolithic.py b/awx/main/tests/old/commands/commands_monolithic.py index e0efcf5326..c0d409c5e0 100644 --- a/awx/main/tests/old/commands/commands_monolithic.py +++ b/awx/main/tests/old/commands/commands_monolithic.py @@ -952,7 +952,8 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertNotEqual(new_inv.groups.count(), 0) self.assertNotEqual(new_inv.total_hosts, 0) self.assertNotEqual(new_inv.total_groups, 0) - self.assertElapsedLessThan(600) + self.assertElapsedLessThan(1800) # TODO: We need to revisit this again to see if we can optimize this back to the sub-600 second range - anoek 2016-04-18 + def _get_ngroups_for_nhosts(self, n): if n > 0: