diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 07f33002aa..5dae65e338 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -340,16 +340,18 @@ class BaseSerializer(serializers.ModelSerializer): return None elif isinstance(obj, User): return obj.date_joined - else: + elif hasattr(obj, 'created'): return obj.created + return None def get_modified(self, obj): if obj is None: return None elif isinstance(obj, User): return obj.last_login # Not actually exposed for User. - else: + elif hasattr(obj, 'modified'): return obj.modified + return None def build_standard_field(self, field_name, model_field): # DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits diff --git a/awx/main/fields.py b/awx/main/fields.py index e116299bcb..d08bbdfb14 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -18,7 +18,6 @@ from django.db.models.fields.related import ( ReverseManyRelatedObjectsDescriptor, ) from django.utils.encoding import smart_text -from django.utils.timezone import now # AWX from awx.main.models.rbac import batch_role_ancestor_rebuilding @@ -92,9 +91,7 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): class ImplicitRoleField(models.ForeignKey): """Implicitly creates a role entry for a resource""" - 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 "" + def __init__(self, parent_role=None, *args, **kwargs): self.parent_role = parent_role kwargs.setdefault('to', 'Role') @@ -104,8 +101,6 @@ class ImplicitRoleField(models.ForeignKey): def deconstruct(self): name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct() - kwargs['role_name'] = self.role_name - kwargs['role_description'] = self.role_description kwargs['parent_role'] = self.parent_role return name, path, args, kwargs @@ -190,11 +185,7 @@ class ImplicitRoleField(models.ForeignKey): if cur_role is None: missing_roles.append( Role_( - created=now(), - modified=now(), role_field=implicit_role_field.name, - name=implicit_role_field.role_name, - description=implicit_role_field.role_description, content_type_id=ct_id, object_id=instance.id ) @@ -208,7 +199,7 @@ class ImplicitRoleField(models.ForeignKey): updates[role.role_field] = role.id role_ids.append(role.id) type(instance).objects.filter(pk=instance.pk).update(**updates) - Role_._simultaneous_ancestry_rebuild(role_ids) + Role_.rebuild_role_ancestor_list(role_ids, []) # Update parentage if necessary for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): @@ -247,12 +238,7 @@ class ImplicitRoleField(models.ForeignKey): if qs.count() >= 1: role = qs[0] else: - role = Role_.objects.create(created=now(), - modified=now(), - role_field=path, - singleton_name=singleton_name, - name=singleton_name, - description=singleton_name) + role = Role_.objects.create(singleton_name=singleton_name, role_field=singleton_name) parents = [role.id] else: parents = resolve_role_field(instance, path) @@ -269,4 +255,4 @@ class ImplicitRoleField(models.ForeignKey): Role_ = get_current_apps().get_model('main', 'Role') child_ids = [x for x in Role_.parents.through.objects.filter(to_role_id__in=role_ids).distinct().values_list('from_role_id', flat=True)] Role_.objects.filter(id__in=role_ids).delete() - Role_._simultaneous_ancestry_rebuild(child_ids) + Role_.rebuild_role_ancestor_list([], child_ids) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index c22e89cde9..896e81558a 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -2,21 +2,21 @@ from __future__ import unicode_literals from django.db import migrations, models -import django.db.models.deletion from django.conf import settings -import taggit.managers import awx.main.fields class Migration(migrations.Migration): dependencies = [ - ('taggit', '0002_auto_20150616_2121'), ('contenttypes', '0002_remove_content_type_name'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('main', '0007_v300_active_flag_removal'), ] operations = [ + # + # Patch up existing + # migrations.RenameField( 'Organization', 'admins', @@ -47,300 +47,6 @@ class Migration(migrations.Migration): name='deprecated_projects', 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=[ - ('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)), - ('description', models.TextField(default=b'', blank=True)), - ('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', 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)), - ('modified_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), - ('parents', models.ManyToManyField(related_name='children', to='main.Role')), - ('implicit_parents', models.TextField(null=False, default=b'[]')), - ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')), - ], - options={ - 'db_table': 'main_rbac_roles', - '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([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field')]), - ), - - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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', b'inventory.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', b'inventory.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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - 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'), - ), - migrations.AddField( - model_name='team', - name='member_role', - 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( - 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', b'auditor_role', b'scm_update_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'admin_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.RenameField( model_name='organization', old_name='projects', @@ -380,4 +86,241 @@ class Migration(migrations.Migration): name='credential', unique_together=set([]), ), + + + # + # New RBAC models and fields + # + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('role_field', models.TextField()), + ('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)), + ('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)), + ('parents', models.ManyToManyField(related_name='children', to='main.Role')), + ('implicit_parents', models.TextField(default=b'[]')), + ('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)), + ('object_id', models.PositiveIntegerField(default=None, null=True)), + + ], + options={ + 'db_table': 'main_rbac_roles', + 'verbose_name_plural': 'roles', + }, + ), + 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()), + ('object_id', models.PositiveIntegerField()), + ('ancestor', models.ForeignKey(related_name='+', to='main.Role')), + ('descendent', models.ForeignKey(related_name='+', to='main.Role')), + ], + options={ + 'db_table': 'main_rbac_role_ancestors', + 'verbose_name_plural': 'role_ancestors', + }, + ), + migrations.AddField( + model_name='role', + name='ancestors', + field=models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role'), + ), + migrations.AlterIndexTogether( + name='roleancestorentry', + index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field'), ('ancestor', 'descendent')]), + ), + migrations.AddField( + model_name='credential', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_auditor'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='credential', + name='owner_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'singleton:system_administrator'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='credential', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'owner_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='credential', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.member_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'adhoc_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_administrator', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'singleton:system_auditor', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.admin_role', b'singleton:system_administrator'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'organization.auditor_role', b'singleton:system_auditor'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='scm_update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='team', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.admin_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='team', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=b'organization.auditor_role', to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='team', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=None, to='main.Role', null=b'True'), + ), + migrations.AddField( + model_name='team', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', null=b'True'), + ), ] diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 0d7aa3ecb7..8710f8c772 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -44,9 +44,7 @@ def migrate_users(apps, schema_editor): logger.info(smart_text(u"found existing role for user: {}".format(user.username))) except Role.DoesNotExist: role = Role.objects.create( - created=now(), - modified=now(), - singleton_name = smart_text(u'{}-admin_role'.format(user.username)), + role_field='admin_role', content_type = user_content_type, object_id = user.id ) @@ -54,14 +52,12 @@ def migrate_users(apps, schema_editor): logger.info(smart_text(u"migrating to new role for user: {}".format(user.username))) if user.is_superuser: - if Role.objects.filter(singleton_name='System Administrator').exists(): - sa_role = Role.objects.get(singleton_name='System Administrator') + if Role.objects.filter(singleton_name='system_administrator').exists(): + sa_role = Role.objects.get(singleton_name='system_administrator') else: sa_role = Role.objects.create( - created=now(), - modified=now(), - singleton_name='System Administrator', - name='System Administrator' + singleton_name='system_administrator', + role_field='system_administrator' ) sa_role.members.add(user) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index d47153285d..11b2a31e87 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -204,27 +204,19 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): help_text=_('Tenant identifier for this credential'), ) owner_role = ImplicitRoleField( - role_name='Credential Owner', - role_description='Owner of the credential', parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], ) auditor_role = ImplicitRoleField( - role_name='Credential Auditor', - role_description='Auditor of the credential', parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], ) 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', - role_description='May read this credential', parent_role=[ 'use_role', 'auditor_role', 'owner_role' ], diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index d192825ec7..ac9a8840cf 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -97,39 +97,25 @@ class Inventory(CommonModel, ResourceMixin): help_text=_('Number of external inventory sources in this inventory with failures.'), ) admin_role = ImplicitRoleField( - role_name='Inventory Administrator', - role_description='May manage this inventory', parent_role='organization.admin_role', ) auditor_role = ImplicitRoleField( - role_name='Inventory Auditor', - role_description='May view but not modify this inventory', parent_role='organization.auditor_role', ) 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', - role_description='May execute jobs against this inventory', parent_role='adhoc_role', ) 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): @@ -531,28 +517,21 @@ class Group(CommonModelNameNotUnique, ResourceMixin): help_text=_('Inventory source(s) that created or modified this group.'), ) admin_role = ImplicitRoleField( - role_name='Inventory Group Administrator', parent_role=['inventory.admin_role', 'parents.admin_role'], ) auditor_role = ImplicitRoleField( - role_name='Inventory Group Auditor', parent_role=['inventory.auditor_role', 'parents.auditor_role'], ) update_role = ImplicitRoleField( - role_name='Inventory Group Updater', 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', 'admin_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', 'adhoc_role'], ) read_role = ImplicitRoleField( - role_name='Inventory Group Executor', parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'], ) @@ -1321,25 +1300,15 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): ) admin_role = ImplicitRoleField( - role_name='CustomInventory Administrator', - role_description='May manage this inventory', parent_role='organization.admin_role', ) - member_role = ImplicitRoleField( - role_name='CustomInventory Member', - role_description='May view but not modify this inventory', parent_role='organization.member_role', ) - auditor_role = ImplicitRoleField( - role_name='CustomInventory Auditor', - 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'], ) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index c48007e24a..c64b1e1f8b 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -226,23 +226,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): default={}, ) admin_role = ImplicitRoleField( - role_name='Job Template Administrator', - role_description='Full access to all settings', parent_role=[('project.admin_role', 'inventory.admin_role')] ) auditor_role = ImplicitRoleField( - role_name='Job Template Auditor', - role_description='Read-only access to all settings', parent_role=[('project.auditor_role', 'inventory.auditor_role')] ) 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', - role_description='May run the job template', parent_role=['execute_role', 'auditor_role', 'admin_role'], ) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 571f9117ab..3d8a06f446 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -53,23 +53,15 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): related_name='deprecated_organizations', ) admin_role = ImplicitRoleField( - role_name='Organization Administrator', - role_description='May manage all aspects of this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ) auditor_role = ImplicitRoleField( - role_name='Organization Auditor', - role_description='May read all settings associated with this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ) member_role = ImplicitRoleField( - role_name='Organization Member', - 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', 'auditor_role'], ) @@ -110,22 +102,13 @@ class Team(CommonModelNameNotUnique, ResourceMixin): related_name='deprecated_teams', ) admin_role = ImplicitRoleField( - role_name='Team Administrator', - role_description='May manage this team', parent_role='organization.admin_role', ) auditor_role = ImplicitRoleField( - role_name='Team Auditor', - role_description='May read all settings associated with this team', parent_role='organization.auditor_role', ) - member_role = ImplicitRoleField( - role_name='Team Member', - role_description='A member of this team', - ) + member_role = ImplicitRoleField() read_role = ImplicitRoleField( - role_name='Read', - role_description='Can view this team', parent_role=['admin_role', 'auditor_role', 'member_role'], ) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 12357fca2e..41145821f4 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -221,34 +221,24 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): blank=True, ) admin_role = ImplicitRoleField( - role_name='Project Administrator', - role_description='May manage this project', parent_role=[ 'organization.admin_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], ) auditor_role = ImplicitRoleField( - role_name='Project Auditor', - role_description='May read all settings associated with this project', parent_role=[ 'organization.auditor_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], ) member_role = ImplicitRoleField( - role_name='Project Member', - role_description='Implies membership within this project', parent_role='admin_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'], ) diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index f757dc580e..64e748a20a 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -29,8 +29,39 @@ __all__ = [ logger = logging.getLogger('awx.main.models.rbac') -ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator' -ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor' +ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='system_administrator' +ROLE_SINGLETON_SYSTEM_AUDITOR='system_auditor' + +role_names = { + 'system_administrator' : 'System Administrator', + 'system_auditor' : 'System Auditor', + 'adhoc_role' : 'Ad Hoc', + 'admin_role' : 'Admin', + 'auditor_role' : 'Auditor', + 'execute_role' : 'Execute', + 'member_role' : 'Member', + 'owner_role' : 'Owner', + 'read_role' : 'Read', + 'scm_update_role' : 'SCM Update', + 'update_role' : 'Update', + 'use_role' : 'Use', +} + +role_descriptions = { + 'system_administrator' : '[TODO] System Administrator', + 'system_auditor' : '[TODO] System Auditor', + 'adhoc_role' : '[TODO] Ad Hoc', + 'admin_role' : '[TODO] Admin', + 'auditor_role' : '[TODO] Auditor', + 'execute_role' : '[TODO] Execute', + 'member_role' : '[TODO] Member', + 'owner_role' : '[TODO] Owner', + 'read_role' : '[TODO] Read', + 'scm_update_role' : '[TODO] SCM Update', + 'update_role' : '[TODO] Update', + 'use_role' : '[TODO] Use', +} + tls = threading.local() # thread local storage @@ -51,23 +82,22 @@ def batch_role_ancestor_rebuilding(allow_nesting=False): try: setattr(tls, 'batch_role_rebuilding', True) if not batch_role_rebuilding: - setattr(tls, 'roles_needing_rebuilding', set()) + setattr(tls, 'additions', set()) + setattr(tls, 'removals', set()) yield finally: setattr(tls, 'batch_role_rebuilding', batch_role_rebuilding) if not batch_role_rebuilding: - rebuild_set = getattr(tls, 'roles_needing_rebuilding') + additions = getattr(tls, 'additions') + removals = getattr(tls, 'removals') with transaction.atomic(): - 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 - # role.rebuild_role_ancestor_list() - delattr(tls, 'roles_needing_rebuilding') + Role.rebuild_role_ancestor_list(list(additions), list(removals)) + delattr(tls, 'additions') + delattr(tls, 'removals') -class Role(CommonModelNameNotUnique): +class Role(models.Model): ''' Role model ''' @@ -77,8 +107,8 @@ class Role(CommonModelNameNotUnique): verbose_name_plural = _('roles') db_table = 'main_rbac_roles' + role_field = models.TextField(null=False) singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True) - role_field = models.TextField(null=False, default='') parents = models.ManyToManyField('Role', related_name='children') implicit_parents = models.TextField(null=False, default='[]') ancestors = models.ManyToManyField( @@ -94,7 +124,7 @@ class Role(CommonModelNameNotUnique): def save(self, *args, **kwargs): super(Role, self).save(*args, **kwargs) - self.rebuild_role_ancestor_list() + self.rebuild_role_ancestor_list([self.id], []) def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) @@ -112,20 +142,36 @@ class Role(CommonModelNameNotUnique): object_id=accessor.id) return self.ancestors.filter(pk__in=roles).exists() - def rebuild_role_ancestor_list(self): + @property + def name(self): + global role_names + return role_names[self.role_field] + + @property + def description(self): + global role_descriptions + return role_descriptions[self.role_field] + + @staticmethod + def rebuild_role_ancestor_list(additions, removals): ''' Updates our `ancestors` map to accurately reflect all of the ancestors for a role You should never need to call this. Signal handlers should be calling this method when the role hierachy changes automatically. - - Note that this method relies on any parents' ancestor list being correct. ''' - Role._simultaneous_ancestry_rebuild([self.id]) - - - @staticmethod - def _simultaneous_ancestry_rebuild(role_ids_to_rebuild): + # The ancestry table + # ================================================= + # + # The role ancestors table denormalizes the parental relations + # between all roles in the system. If you have role A which is a + # parent of B which is a parent of C, then the ancestors table will + # contain a row noting that B is a descendent of A, and two rows for + # denoting that C is a descendent of both A and B. In addition to + # storing entries for each descendent relationship, we also store an + # entry that states that C is a 'descendent' of itself, C. This makes + # usage of this table simple in our queries as it enables us to do + # straight joins where we would have to do unions otherwise. # # The simple version of what this function is doing # ================================================= @@ -163,37 +209,18 @@ class Role(CommonModelNameNotUnique): # # 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 + # our self reference entry where ancestor_id = descendent_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 deletes all entries in the ancestor table that + # should no longer be there (as determined by the NOT EXISTS query, + # which checks to see if the ancestor is still an ancestor of one + # or more of our parents) # - # 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. + # The INSERT query computes the list of what our ancestor maps should + # be, and inserts any missing entries. # # 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 @@ -205,18 +232,17 @@ class Role(CommonModelNameNotUnique): # # - if len(role_ids_to_rebuild) == 0: + if len(additions) == 0 and len(removals) == 0: return global tls batch_role_rebuilding = getattr(tls, 'batch_role_rebuilding', False) if batch_role_rebuilding: - roles_needing_rebuilding = getattr(tls, 'roles_needing_rebuilding') - roles_needing_rebuilding.update(set(role_ids_to_rebuild)) + getattr(tls, 'additions').update(set(additions)) + getattr(tls, 'removals').update(set(removals)) return - cursor = connection.cursor() loop_ct = 0 @@ -226,85 +252,98 @@ class Role(CommonModelNameNotUnique): 'roles_table': Role._meta.db_table, } + # SQLlite has a 1M sql statement limit.. since the django sqllite + # driver isn't letting us pass in the ids through the preferred + # parameter binding system, this function exists to obey this. + # est max 12 bytes per number, used up to 2 times in a query, + # minus 4k of padding for the other parts of the query, leads us + # to the magic number of 41496, or 40000 for a nice round number def split_ids_for_sqlite(role_ids): - for i in xrange(0, len(role_ids), 999): - yield role_ids[i:i + 999] - - while role_ids_to_rebuild: - if loop_ct > 1000: - raise Exception('Ancestry role rebuilding error: infinite loop detected') - loop_ct += 1 - - 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 - - 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 - - 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 - - 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 - - if insert_ct == 0 and delete_ct == 0: - break - - 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) + for i in xrange(0, len(role_ids), 40000): + yield role_ids[i:i + 40000] + with transaction.atomic(): + while len(additions) > 0 or len(removals) > 0: + if loop_ct > 100: + raise Exception('Role ancestry rebuilding error: infinite loop detected') + loop_ct += 1 + + delete_ct = 0 + if len(removals) > 0: + for ids in split_ids_for_sqlite(removals): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE descendent_id IN (%(ids)s) + AND descendent_id != ancestor_id + AND NOT EXISTS ( + SELECT 1 + FROM %(parents_table)s as parents + INNER JOIN %(ancestors_table)s as inner_ancestors + ON (parents.to_role_id = inner_ancestors.descendent_id) + WHERE parents.from_role_id = %(ancestors_table)s.descendent_id + AND %(ancestors_table)s.ancestor_id = inner_ancestors.ancestor_id + ) + ''' % sql_params) + + delete_ct += cursor.rowcount + + insert_ct = 0 + if len(additions) > 0: + for ids in split_ids_for_sqlite(additions): + 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 roles.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 %(roles_table)s as roles + INNER JOIN %(parents_table)s as parents + ON (parents.from_role_id = roles.id) + INNER JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE roles.id IN (%(ids)s) + + 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 + WHERE NOT EXISTS ( + SELECT 1 FROM %(ancestors_table)s + WHERE %(ancestors_table)s.descendent_id = new_ancestry_list.from_id + AND %(ancestors_table)s.ancestor_id = new_ancestry_list.to_id + ) + + ''' % sql_params) + insert_ct += cursor.rowcount + + if insert_ct == 0 and delete_ct == 0: + break + + new_additions = set() + for ids in split_ids_for_sqlite(additions): + sql_params['ids'] = ','.join(str(x) for x in ids) + # get all children for the roles we're operating on + cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params) + new_additions.update([row[0] for row in cursor.fetchall()]) + additions = list(new_additions) + + new_removals = set() + for ids in split_ids_for_sqlite(removals): + sql_params['ids'] = ','.join(str(x) for x in ids) + # get all children for the roles we're operating on + cursor.execute('SELECT DISTINCT from_role_id FROM %(parents_table)s WHERE to_role_id IN (%(ids)s)' % sql_params) + new_removals.update([row[0] for row in cursor.fetchall()]) + removals = list(new_removals) @staticmethod @@ -313,7 +352,7 @@ class Role(CommonModelNameNotUnique): @staticmethod def singleton(name): - role, _ = Role.objects.get_or_create(singleton_name=name, name=name) + role, _ = Role.objects.get_or_create(singleton_name=name, role_field=name) return role def is_ancestor_of(self, role): @@ -328,6 +367,7 @@ class RoleAncestorEntry(models.Model): index_together = [ ("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource ("ancestor", "content_type_id", "role_field"), # used by accessible_objects + ("ancestor", "descendent"), # used by rebuild_role_ancestor_list in the NOT EXISTS clauses. ] descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') diff --git a/awx/main/signals.py b/awx/main/signals.py index e4893d34c2..068efb4ece 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -108,12 +108,17 @@ def emit_update_inventory_on_created_or_deleted(sender, **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 action in ['post_add', 'post_remove', 'post_clear']: + if action == 'post_add': if reverse: - for id in pk_set: - model.objects.get(id=id).rebuild_role_ancestor_list() + model.rebuild_role_ancestor_list(list(pk_set), []) else: - instance.rebuild_role_ancestor_list() + model.rebuild_role_ancestor_list([instance.id], []) + + if action in ['post_remove', 'post_clear']: + if reverse: + model.rebuild_role_ancestor_list([], list(pk_set)) + else: + model.rebuild_role_ancestor_list([], [instance.id]) 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' @@ -127,11 +132,10 @@ def create_user_role(instance, **kwargs): Role.objects.get( content_type=ContentType.objects.get_for_model(instance), object_id=instance.id, - name = 'User Admin' + role_field='admin_role' ) except Role.DoesNotExist: role = Role.objects.create( - name = 'User Admin', role_field='admin_role', content_object = instance, ) diff --git a/awx/main/tests/functional/api/test_resource_access_lists.py b/awx/main/tests/functional/api/test_resource_access_lists.py index 75e55fd8ca..9d8d95c98a 100644 --- a/awx/main/tests/functional/api/test_resource_access_lists.py +++ b/awx/main/tests/functional/api/test_resource_access_lists.py @@ -1,6 +1,7 @@ import pytest from django.core.urlresolvers import reverse +from awx.main.models import Role @pytest.mark.django_db def test_indirect_access_list(get, organization, project, team_factory, user, admin): @@ -53,5 +54,5 @@ def test_indirect_access_list(get, organization, project, team_factory, user, ad assert org_admin_team_member_entry['team_name'] == org_admin_team.name admin_entry = admin_res['summary_fields']['indirect_access'][0]['role'] - assert admin_entry['name'] == 'System Administrator' + assert admin_entry['name'] == Role.singleton('system_administrator').name diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 4b481895d6..33c24f3cc6 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -36,7 +36,6 @@ from awx.main.models.organization import ( Team, ) -from awx.main.models.rbac import Role from awx.main.models.notifications import Notifier ''' @@ -193,11 +192,6 @@ def notifier(organization): notification_type="webhook", notification_configuration=dict(url="http://localhost", headers={"Test": "Header"})) - -@pytest.fixture -def role(): - return Role.objects.create(name='role') - @pytest.fixture def admin(user): return user('admin', True) diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index 3e080c8453..9c1fd34356 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -10,6 +10,10 @@ def mock_feature_enabled(feature, bypass_database=None): #@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) +@pytest.fixture +def role(): + return Role.objects.create() + # # /roles @@ -85,7 +89,7 @@ def test_get_user_roles_list(get, admin): response = get(url, admin) assert response.status_code == 200 roles = response.data - assert roles['count'] > 0 # 'System Administrator' role if nothing else + assert roles['count'] > 0 # 'system_administrator' role if nothing else @pytest.mark.django_db def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob): diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 537052afd2..8001cb6d71 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -11,8 +11,8 @@ from awx.main.models import ( @pytest.mark.django_db def test_auto_inheritance_by_children(organization, alice): - A = Role.objects.create(name='A', role_field='') - B = Role.objects.create(name='B', role_field='') + A = Role.objects.create() + B = Role.objects.create() A.members.add(alice) assert alice not in organization.admin_role @@ -38,8 +38,8 @@ def test_auto_inheritance_by_children(organization, alice): @pytest.mark.django_db def test_auto_inheritance_by_parents(organization, alice): - A = Role.objects.create(name='A') - B = Role.objects.create(name='B') + A = Role.objects.create() + B = Role.objects.create() A.members.add(alice) assert alice not in organization.admin_role @@ -58,9 +58,9 @@ def test_auto_inheritance_by_parents(organization, alice): @pytest.mark.django_db def test_accessible_objects(organization, alice, bob): - A = Role.objects.create(name='A') + A = Role.objects.create() A.members.add(alice) - B = Role.objects.create(name='B') + B = Role.objects.create() B.members.add(alice) B.members.add(bob) @@ -118,7 +118,7 @@ def test_auto_field_adjustments(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 = Role.objects.create() child.parents.add(delorg.admin_role) delorg.admin_role.members.add(alice) @@ -129,14 +129,14 @@ def test_implicit_deletes(alice): 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() - n_system_admin_children = Role.singleton('System Administrator').children.count() + n_system_admin_children = Role.singleton('system_administrator').children.count() 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 Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1) + assert Role.singleton('system_administrator').children.count() == (n_system_admin_children - 1) assert child.ancestors.count() == 1 assert child.ancestors.all()[0] == child @@ -152,11 +152,11 @@ def test_content_object(user): 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') - B = Role.objects.create(name='B') - C = Role.objects.create(name='C') - D = Role.objects.create(name='D') + X = Role.objects.create() + A = Role.objects.create() + B = Role.objects.create() + C = Role.objects.create() + D = Role.objects.create() A.children.add(B) A.children.add(D) diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index d2e504645a..a225154d21 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -148,7 +148,7 @@ def test_project_user_project(user_project, project, user): def test_project_accessible_by_sa(user, project): u = user('systemadmin', is_superuser=True) # 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) + Role.singleton('system_administrator').members.remove(u) assert u not in project.read_role rbac.migrate_organization(apps, None) diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 8e620771f5..c5959a2c32 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -13,7 +13,7 @@ def test_user_admin(user_project, project, user): joe = user(username, is_superuser = False) admin = user('admin', is_superuser = True) - sa = Role.singleton('System Administrator') + sa = Role.singleton('system_administrator') # this should happen automatically with our signal assert sa.members.filter(id=admin.id).exists() is True diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py index 5a0df82da5..cf26cb3da3 100755 --- a/tools/data_generators/rbac_dummy_data_generator.py +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -114,8 +114,8 @@ class Rollback(Exception): try: - with batch_role_ancestor_rebuilding(): - with transaction.atomic(): + with transaction.atomic(): + with batch_role_ancestor_rebuilding(): 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')