diff --git a/awx/api/views.py b/awx/api/views.py index 96c3bf396e..1a1ce35346 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1555,7 +1555,7 @@ class InventoryRootGroupsList(SubListCreateAttachDetachAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) + qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator return qs & parent.root_groups class BaseVariableData(RetrieveUpdateAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index a65ed9911e..2446c94553 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -322,7 +322,7 @@ 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': True}) qs = qs.select_related('created_by', 'modified_by', 'organization') return qs diff --git a/awx/main/fields.py b/awx/main/fields.py index 7785cf1f72..22210124f0 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -23,7 +23,7 @@ from django.db.transaction import TransactionManagementError # AWX -from awx.main.models.rbac import RolePermission, Role +from awx.main.models.rbac import RolePermission, Role, batch_role_ancestor_rebuilding __all__ = ['AutoOneToOneField', 'ImplicitRoleField'] @@ -126,7 +126,8 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): if self.permissions is not None: permissions = RolePermission( role=role, - resource=instance + resource=instance, + auto_generated=True ) if 'all' in self.permissions and self.permissions['all']: @@ -279,12 +280,11 @@ class ImplicitRoleField(models.ForeignKey): for parent in parents: new_parents.add(parent) - Role.pause_role_ancestor_rebuilding() - for role in original_parents - new_parents: - this_role.parents.remove(role) - for role in new_parents - original_parents: - this_role.parents.add(role) - Role.unpause_role_ancestor_rebuilding() + with batch_role_ancestor_rebuilding(): + for role in original_parents - new_parents: + this_role.parents.remove(role) + for role in new_parents - original_parents: + this_role.parents.add(role) setattr(self, '__original_parent_roles', new_parents) @@ -293,4 +293,4 @@ class ImplicitRoleField(models.ForeignKey): children = [c for c in this_role.children.all()] this_role.delete() for child in children: - children.rebuild_role_ancestor_list() + child.rebuild_role_ancestor_list() diff --git a/awx/main/management/commands/generate_dummy_data.py b/awx/main/management/commands/generate_dummy_data.py new file mode 100644 index 0000000000..e054940510 --- /dev/null +++ b/awx/main/management/commands/generate_dummy_data.py @@ -0,0 +1,352 @@ +# 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), user=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), team=team) + 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/awx/main/migrations/0006_v300_rbac_changes.py b/awx/main/migrations/0006_v300_rbac_changes.py index d9313fe8aa..c6a9ad6da4 100644 --- a/awx/main/migrations/0006_v300_rbac_changes.py +++ b/awx/main/migrations/0006_v300_rbac_changes.py @@ -63,6 +63,7 @@ class Migration(migrations.Migration): ('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)), @@ -85,6 +86,11 @@ class Migration(migrations.Migration): name='owner_role', field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), ), + migrations.AddField( + model_name='credential', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + ), migrations.AddField( model_name='credential', name='usage_role', diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index ec47cb1fbb..9ae6b47298 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -16,6 +16,10 @@ from awx.main.constants import CLOUD_PROVIDERS from awx.main.utils import decrypt_field from awx.main.models.base import * # noqa from awx.main.models.mixins import ResourceMixin +from awx.main.models.rbac import ( + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, + ROLE_SINGLETON_SYSTEM_AUDITOR, +) __all__ = ['Credential'] @@ -158,9 +162,20 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): owner_role = ImplicitRoleField( role_name='Credential Owner', role_description='Owner of the credential', - parent_role='team.admin_role', + parent_role=[ + 'team.admin_role', + 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, + ], permissions = {'all': True} ) + auditor_role = ImplicitRoleField( + role_name='Credential Auditor', + role_description='Auditor of the credential', + 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', diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 0475a4b166..644ffa1315 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -3,9 +3,11 @@ # Python import logging +import threading +import contextlib # Django -from django.db import models +from django.db import models, transaction from django.db.models import Q from django.db.models.aggregates import Max from django.core.urlresolvers import reverse @@ -19,6 +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', @@ -30,12 +33,43 @@ logger = logging.getLogger('awx.main.models.rbac') ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator' ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor' -role_rebuilding_paused = False -roles_needing_rebuilding = set() - 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 +def batch_role_ancestor_rebuilding(allow_nesting=False): + ''' + Batches the role ancestor rebuild work necessary whenever role-role + relations change. This can result in a big speedup when performing + any bulk manipulation. + + WARNING: Calls to anything related to checking access/permissions + while within the context of the batch_role_ancestor_rebuilding will + likely not work. + ''' + + batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False) + + try: + setattr(tls, 'batch_role_rebuilding', True) + if not batch_role_rebuilding: + setattr(tls, 'roles_needing_rebuilding', set()) + yield + + finally: + setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding) + 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() + delattr(tls, 'roles_needing_rebuilding') + + class Role(CommonModelNameNotUnique): ''' Role model @@ -61,35 +95,6 @@ class Role(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) - @staticmethod - def pause_role_ancestor_rebuilding(): - ''' - Pauses role ancestor list updating. This is useful when you're making - many changes to the same roles, for example doing bulk inserts or - making many changes to the same object in succession. - - Note that the unpause_role_ancestor_rebuilding MUST be called within - the same execution context (preferably within the same transaction), - otherwise the RBAC role ancestor hierarchy will not be properly - updated. - ''' - - global role_rebuilding_paused - role_rebuilding_paused = True - - @staticmethod - def unpause_role_ancestor_rebuilding(): - ''' - Unpauses the role ancestor list updating. This will will rebuild all - roles that need updating since the last call to - pause_role_ancestor_rebuilding and bring everything back into sync. - ''' - global role_rebuilding_paused - global roles_needing_rebuilding - role_rebuilding_paused = False - for role in Role.objects.filter(id__in=list(roles_needing_rebuilding)).all(): - role.rebuild_role_ancestor_list() - roles_needing_rebuilding = set() def rebuild_role_ancestor_list(self): ''' @@ -100,9 +105,11 @@ class Role(CommonModelNameNotUnique): Note that this method relies on any parents' ancestor list being correct. ''' - global role_rebuilding_paused, roles_needing_rebuilding + global tls + batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False) - if role_rebuilding_paused: + if batch_role_rebuilding: + roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding') roles_needing_rebuilding.add(self.id) return @@ -160,6 +167,7 @@ class RolePermission(CreatedModifiedModel): 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) diff --git a/awx/main/signals.py b/awx/main/signals.py index ddfc792f2a..eb4806ffe5 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -133,6 +133,7 @@ def create_user_role(instance, **kwargs): 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, ) @@ -152,6 +153,102 @@ 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) post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Group) @@ -168,6 +265,8 @@ 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) diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index d5638878a2..d7657344f5 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -134,11 +134,14 @@ def test_auto_field_adjuments(organization, inventory, team, alice): def test_implicit_deletes(alice): 'Ensures implicit resources and roles delete themselves' delorg = Organization.objects.create(name='test-org') + child = Role.objects.create(name='child-role') + child.parents.add(delorg.admin_role) delorg.admin_role.members.add(alice) admin_role_id = delorg.admin_role.id auditor_role_id = delorg.auditor_role.id + assert child.ancestors.count() > 1 assert Role.objects.filter(id=admin_role_id).count() == 1 assert Role.objects.filter(id=auditor_role_id).count() == 1 n_alice_roles = alice.roles.count() @@ -152,6 +155,9 @@ def test_implicit_deletes(alice): 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 + @pytest.mark.django_db def test_content_object(user): diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 6a22273755..a38faf2643 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -1,7 +1,7 @@ import pytest from awx.main.migrations import _rbac as rbac -from awx.main.models import Permission +from awx.main.models import Permission, Host from awx.main.access import InventoryAccess from django.apps import apps @@ -232,3 +232,42 @@ def test_access_auditor(organization, inventory, user): assert not access.can_run_ad_hoc_commands(inventory) + +@pytest.mark.django_db +def test_host_access(organization, inventory, user, group): + other_inventory = organization.inventories.create(name='other-inventory') + inventory_admin = user('inventory_admin', False) + my_group = group('my-group') + not_my_group = group('not-my-group') + group_admin = user('group_admin', False) + + + 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 + + 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 + + my_group.hosts.remove(h1) + + assert h1.accessible_by(inventory_admin, {'read': True}) + assert h1.accessible_by(group_admin, {'read': True}) 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 + + + diff --git a/awx/main/tests/job_base.py b/awx/main/tests/job_base.py index 54c3462fbc..f48380f60b 100644 --- a/awx/main/tests/job_base.py +++ b/awx/main/tests/job_base.py @@ -66,68 +66,68 @@ class BaseJobTestMixin(BaseTestMixin): # Alex is Sue's IT assistant who can also administer all of the # organizations. self.user_alex = self.make_user('alex') - self.org_eng.admins.add(self.user_alex) - self.org_sup.admins.add(self.user_alex) - self.org_ops.admins.add(self.user_alex) + self.org_eng.admin_role.members.add(self.user_alex) + self.org_sup.admin_role.members.add(self.user_alex) + self.org_ops.admin_role.members.add(self.user_alex) # Bob is the head of engineering. He's an admin for engineering, but # also a user within the operations organization (so he can see the # results if things go wrong in production). self.user_bob = self.make_user('bob') - self.org_eng.admins.add(self.user_bob) - self.org_ops.users.add(self.user_bob) + self.org_eng.admin_role.members.add(self.user_bob) + self.org_ops.member_role.members.add(self.user_bob) # Chuck is the lead engineer. He has full reign over engineering, but # no other organizations. self.user_chuck = self.make_user('chuck') - self.org_eng.admins.add(self.user_chuck) + self.org_eng.admin_role.members.add(self.user_chuck) # Doug is the other engineer working under Chuck. He can write # playbooks and check them, but Chuck doesn't quite think he's ready to # run them yet. Poor Doug. self.user_doug = self.make_user('doug') - self.org_eng.users.add(self.user_doug) + self.org_eng.member_role.members.add(self.user_doug) # Juan is another engineer working under Chuck. He has a little more freedom # to run playbooks but can't create job templates self.user_juan = self.make_user('juan') - self.org_eng.users.add(self.user_juan) + self.org_eng.member_role.members.add(self.user_juan) # Hannibal is Chuck's right-hand man. Chuck usually has him create the job # templates that the rest of the team will use self.user_hannibal = self.make_user('hannibal') - self.org_eng.users.add(self.user_hannibal) + self.org_eng.member_role.members.add(self.user_hannibal) # Eve is the head of support. She can also see what goes on in # operations to help them troubleshoot problems. self.user_eve = self.make_user('eve') - self.org_sup.admins.add(self.user_eve) - self.org_ops.users.add(self.user_eve) + self.org_sup.admin_role.members.add(self.user_eve) + self.org_ops.member_role.members.add(self.user_eve) # Frank is the other support guy. self.user_frank = self.make_user('frank') - self.org_sup.users.add(self.user_frank) + self.org_sup.member_role.members.add(self.user_frank) # Greg is the head of operations. self.user_greg = self.make_user('greg') - self.org_ops.admins.add(self.user_greg) + self.org_ops.admin_role.members.add(self.user_greg) # Holly is an operations engineer. self.user_holly = self.make_user('holly') - self.org_ops.users.add(self.user_holly) + self.org_ops.member_role.members.add(self.user_holly) # Iris is another operations engineer. self.user_iris = self.make_user('iris') - self.org_ops.users.add(self.user_iris) + self.org_ops.member_role.members.add(self.user_iris) # Randall and Billybob are new ops interns that ops uses to test # their playbooks and inventory self.user_randall = self.make_user('randall') - self.org_ops.users.add(self.user_randall) + self.org_ops.member_role.members.add(self.user_randall) # He works with Randall self.user_billybob = self.make_user('billybob') - self.org_ops.users.add(self.user_billybob) + self.org_ops.member_role.members.add(self.user_billybob) # Jim is the newest intern. He can login, but can't do anything quite yet # except make everyone else fresh coffee. @@ -218,15 +218,15 @@ class BaseJobTestMixin(BaseTestMixin): created_by=self.user_sue) self.team_ops_east.projects.add(self.proj_prod) self.team_ops_east.projects.add(self.proj_prod_east) - self.team_ops_east.users.add(self.user_greg) - self.team_ops_east.users.add(self.user_holly) + self.team_ops_east.member_role.members.add(self.user_greg) + self.team_ops_east.member_role.members.add(self.user_holly) self.team_ops_west = self.org_ops.teams.create( name='westerners', created_by=self.user_sue) self.team_ops_west.projects.add(self.proj_prod) self.team_ops_west.projects.add(self.proj_prod_west) - self.team_ops_west.users.add(self.user_greg) - self.team_ops_west.users.add(self.user_iris) + self.team_ops_west.member_role.members.add(self.user_greg) + self.team_ops_west.member_role.members.add(self.user_iris) # The south team is no longer active having been folded into the east team # FIXME: This code can be removed (probably) @@ -240,7 +240,7 @@ class BaseJobTestMixin(BaseTestMixin): # active=False, #) #self.team_ops_south.projects.add(self.proj_prod) - #self.team_ops_south.users.add(self.user_greg) + #self.team_ops_south.member_role.members.add(self.user_greg) # The north team is going to be deleted self.team_ops_north = self.org_ops.teams.create( @@ -248,7 +248,7 @@ class BaseJobTestMixin(BaseTestMixin): created_by=self.user_sue, ) self.team_ops_north.projects.add(self.proj_prod) - self.team_ops_north.users.add(self.user_greg) + self.team_ops_north.member_role.members.add(self.user_greg) # The testers team are interns that can only check playbooks but can't # run them @@ -257,8 +257,8 @@ class BaseJobTestMixin(BaseTestMixin): created_by=self.user_sue, ) self.team_ops_testers.projects.add(self.proj_prod) - self.team_ops_testers.users.add(self.user_randall) - self.team_ops_testers.users.add(self.user_billybob) + self.team_ops_testers.member_role.members.add(self.user_randall) + self.team_ops_testers.member_role.members.add(self.user_billybob) # Each user has his/her own set of credentials. from awx.main.tests.data.ssh import (TEST_SSH_KEY_DATA, diff --git a/awx/main/tests/old/projects.py b/awx/main/tests/old/projects.py index 2c715e312a..04de530dad 100644 --- a/awx/main/tests/old/projects.py +++ b/awx/main/tests/old/projects.py @@ -446,7 +446,7 @@ class ProjectsTest(BaseTransactionTest): self.post(team_users, data=dict(username='attempted_superuser_create', password='thepassword', is_superuser=True), expect=201, auth=self.get_super_credentials()) - self.assertEqual(Team.objects.get(pk=team.pk).users.count(), 5) + self.assertEqual(Team.objects.get(pk=team.pk).member_role.members.count(), 5) # can remove users from teams for x in all_users['results']: diff --git a/awx/sso/backends.py b/awx/sso/backends.py index b2c11be2d6..9e227624ec 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -210,11 +210,11 @@ def on_populate_user(sender, **kwargs): remove = bool(org_opts.get('remove', False)) admins_opts = org_opts.get('admins', None) remove_admins = bool(org_opts.get('remove_admins', remove)) - _update_m2m_from_groups(user, ldap_user, org.admins, admins_opts, + _update_m2m_from_groups(user, ldap_user, org.admin_role.members, admins_opts, remove_admins) users_opts = org_opts.get('users', None) remove_users = bool(org_opts.get('remove_users', remove)) - _update_m2m_from_groups(user, ldap_user, org.users, users_opts, + _update_m2m_from_groups(user, ldap_user, org.member_role.members, users_opts, remove_users) # Update team membership based on group memberships. @@ -226,7 +226,7 @@ def on_populate_user(sender, **kwargs): team, created = Team.objects.get_or_create(name=team_name, organization=org) users_opts = team_opts.get('users', None) remove = bool(team_opts.get('remove', False)) - _update_m2m_from_groups(user, ldap_user, team.users, users_opts, + _update_m2m_from_groups(user, ldap_user, team.member_role.users, users_opts, remove) # Update user profile to store LDAP DN. diff --git a/awx/sso/pipeline.py b/awx/sso/pipeline.py index f3e8987d17..a79aecacb0 100644 --- a/awx/sso/pipeline.py +++ b/awx/sso/pipeline.py @@ -98,12 +98,12 @@ def update_user_orgs(backend, details, user=None, *args, **kwargs): remove = bool(org_opts.get('remove', False)) admins_expr = org_opts.get('admins', None) remove_admins = bool(org_opts.get('remove_admins', remove)) - _update_m2m_from_expression(user, org.admins, admins_expr, remove_admins) + _update_m2m_from_expression(user, org.admin_role.members, admins_expr, remove_admins) # Update org users from expression(s). users_expr = org_opts.get('users', None) remove_users = bool(org_opts.get('remove_users', remove)) - _update_m2m_from_expression(user, org.users, users_expr, remove_users) + _update_m2m_from_expression(user, org.member_role.members, users_expr, remove_users) def update_user_teams(backend, details, user=None, *args, **kwargs): @@ -134,4 +134,4 @@ def update_user_teams(backend, details, user=None, *args, **kwargs): team = Team.objects.get_or_create(name=team_name, organization=org)[0] users_expr = team_opts.get('users', None) remove = bool(team_opts.get('remove', False)) - _update_m2m_from_expression(user, team.users, users_expr, remove) + _update_m2m_from_expression(user, team.member_role.members, users_expr, remove)