diff --git a/awx/main/fields.py b/awx/main/fields.py index 1102ce4238..7e57c29c7d 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -1,6 +1,8 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. +import json + # Django from django.db.models.signals import ( pre_save, @@ -17,10 +19,11 @@ 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 RolePermission, Role, batch_role_ancestor_rebuilding - +from awx.main.models.rbac import batch_role_ancestor_rebuilding +from awx.main.utils import get_current_apps __all__ = ['AutoOneToOneField', 'ImplicitRoleField'] @@ -65,10 +68,14 @@ def resolve_role_field(obj, field): else: return [] + if obj is None: + return [] + if len(field_components) == 1: - if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role: - raise Exception(smart_text('{} refers to a {}, not an ImplicitRoleField or Role'.format(field, type(obj)))) - ret.append(obj) + Role_ = get_current_apps().get_model('main', 'Role') + if type(obj) is not Role_: + raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj)))) + ret.append(obj.id) else: if type(obj) is ManyRelatedObjectsDescriptor: for o in obj.all(): @@ -97,6 +104,14 @@ class ImplicitRoleField(models.ForeignKey): kwargs.setdefault('null', 'True') super(ImplicitRoleField, self).__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct() + kwargs['role_name'] = self.role_name + kwargs['role_description'] = self.role_description + kwargs['permissions'] = self.permissions + kwargs['parent_role'] = self.parent_role + return name, path, args, kwargs + def contribute_to_class(self, cls, name): super(ImplicitRoleField, self).contribute_to_class(cls, name) setattr(cls, self.name, ImplicitRoleDescriptor(self)) @@ -165,8 +180,11 @@ class ImplicitRoleField(models.ForeignKey): def _create_role_instance_if_not_exists(self, instance): role = getattr(instance, self.name, None) if role: - return role - role = Role.objects.create( + return + Role_ = get_current_apps().get_model('main', 'Role') + role = Role_.objects.create( + created=now(), + modified=now(), name=self.role_name, description=self.role_description ) @@ -178,9 +196,16 @@ class ImplicitRoleField(models.ForeignKey): role.save() if self.permissions is not None: - permissions = RolePermission( + RolePermission_ = get_current_apps().get_model('main', 'RolePermission') + ContentType = get_current_apps().get_model('contenttypes', "ContentType") + instance_content_type = ContentType.objects.get_for_model(instance) + + permissions = RolePermission_( + created=now(), + modified=now(), role=role, - resource=instance, + content_type=instance_content_type, + object_id=instance.id, auto_generated=True ) @@ -203,38 +228,24 @@ class ImplicitRoleField(models.ForeignKey): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): implicit_role_field._create_role_instance_if_not_exists(instance) - original_parent_roles = dict() - if instance.pk: - original = instance.__class__.objects.get(pk=instance.pk) - for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): - original_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(original) - - setattr(instance, '__original_parent_roles', original_parent_roles) - - def _post_save(self, instance, created, *args, **kwargs): if created: for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): implicit_role_field._patch_role_content_object_and_grant_permissions(instance) - original_parent_roles = getattr(instance, '__original_parent_roles') - - if created: - for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): - original_parent_roles[implicit_role_field.name] = set() - - new_parent_roles = dict() - for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): - new_parent_roles[implicit_role_field.name] = implicit_role_field._resolve_parent_roles(instance) - setattr(instance, '__original_parent_roles', new_parent_roles) - with batch_role_ancestor_rebuilding(): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): cur_role = getattr(instance, implicit_role_field.name) - original_parents = original_parent_roles[implicit_role_field.name] - new_parents = new_parent_roles[implicit_role_field.name] + original_parents = set(json.loads(cur_role.implicit_parents)) + new_parents = implicit_role_field._resolve_parent_roles(instance) cur_role.parents.remove(*list(original_parents - new_parents)) cur_role.parents.add(*list(new_parents - original_parents)) + new_parents_list = list(new_parents) + new_parents_list.sort() + new_parents_json = json.dumps(new_parents_list) + if cur_role.implicit_parents != new_parents_json: + cur_role.implicit_parents = new_parents_json + cur_role.save() def _resolve_parent_roles(self, instance): @@ -245,7 +256,18 @@ class ImplicitRoleField(models.ForeignKey): parent_roles = set() for path in paths: if path.startswith("singleton:"): - parents = [Role.singleton(path[10:])] + singleton_name = path[10:] + Role_ = get_current_apps().get_model('main', 'Role') + qs = Role_.objects.filter(singleton_name=singleton_name) + if qs.count() >= 1: + role = qs[0] + else: + role = Role_.objects.create(created=now(), + modified=now(), + singleton_name=singleton_name, + name=singleton_name, + description=singleton_name) + parents = [role.id] else: parents = resolve_role_field(instance, path) for parent in parents: diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index bbdd9fa540..8848bbabd7 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -7,7 +7,6 @@ from django.conf import settings import taggit.managers import awx.main.fields - class Migration(migrations.Migration): dependencies = [ @@ -38,6 +37,16 @@ class Migration(migrations.Migration): 'projects', 'deprecated_projects', ), + migrations.AddField( + model_name='project', + name='organization', + field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True), + ), + migrations.AlterField( + model_name='team', + name='deprecated_projects', + field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True), + ), migrations.CreateModel( name='Role', @@ -55,6 +64,7 @@ class Migration(migrations.Migration): ('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.ManyToManyField(related_name='implicit_children', to='main.Role')), ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')), ], options={ @@ -86,146 +96,149 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'permissions', }, ), + + migrations.AddField( + model_name='credential', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True', permissions={b'read': True}), + ), migrations.AddField( model_name='credential', 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'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True', permissions={b'all': True}), ), migrations.AddField( model_name='credential', name='usage_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True', permissions={b'use': True}), + ), + migrations.AddField( + model_name='custominventoryscript', + name='admin_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True', permissions={b'all': True}), + ), + migrations.AddField( + model_name='custominventoryscript', + name='auditor_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True', permissions={b'read': True}), + ), + migrations.AddField( + model_name='custominventoryscript', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='group', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True', permissions={b'all': True}), ), migrations.AddField( model_name='group', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='group', name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.executor_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( model_name='group', name='updater_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.updater_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True', permissions={b'read': True, b'write': True, b'create': True, b'use': True}), ), migrations.AddField( model_name='inventory', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True', permissions={b'all': True}), ), migrations.AddField( model_name='inventory', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='inventory', name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( model_name='inventory', name='updater_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True', permissions={b'read': True, b'update': True}), ), migrations.AddField( model_name='inventory', name='usage_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), - ), - migrations.AddField( - model_name='custominventoryscript', - name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), - ), - migrations.AddField( - model_name='custominventoryscript', - name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), - ), - migrations.AddField( - model_name='custominventoryscript', - name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True', permissions={b'use': True}), ), migrations.AddField( model_name='jobtemplate', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True', permissions={b'all': True}), ), migrations.AddField( model_name='jobtemplate', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='jobtemplate', name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True', permissions={b'read': True, b'execute': True}), ), migrations.AddField( model_name='organization', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), ), migrations.AddField( model_name='organization', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='project', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True', permissions={b'all': True}), ), migrations.AddField( model_name='project', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='project', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='project', name='scm_update_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True', permissions={b'scm_update': True}), ), migrations.AddField( model_name='team', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), ), migrations.AddField( model_name='team', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True', permissions={b'read': True}), ), migrations.AddField( model_name='team', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', parent_role=b'admin_role', to='main.Role', role_name=b'Team Member', null=b'True', permissions={b'read': True}), ), + + migrations.AlterIndexTogether( name='rolepermission', index_together=set([('content_type', 'object_id')]), @@ -240,11 +253,6 @@ class Migration(migrations.Migration): name='deprecated_projects', field=models.ManyToManyField(related_name='deprecated_organizations', to='main.Project', blank=True), ), - migrations.AddField( - model_name='project', - name='organization', - field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True), - ), migrations.RenameField( 'Credential', 'team', diff --git a/awx/main/migrations/0009_v300_rbac_migrations.py b/awx/main/migrations/0009_v300_rbac_migrations.py index b652c1067a..9c7f7d8dd7 100644 --- a/awx/main/migrations/0009_v300_rbac_migrations.py +++ b/awx/main/migrations/0009_v300_rbac_migrations.py @@ -12,6 +12,7 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(rbac.init_rbac_migration), migrations.RunPython(rbac.migrate_users), migrations.RunPython(rbac.migrate_organization), migrations.RunPython(rbac.migrate_team), diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 318c6d4667..32c399df81 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -2,11 +2,12 @@ import logging from django.utils.encoding import smart_text from django.db.models import Q +from django.utils.timezone import now from collections import defaultdict -from awx.main.utils import getattrd -import _old_access as old_access +from awx.main.utils import getattrd, set_current_apps +import _old_access as old_access logger = logging.getLogger(__name__) def log_migration(wrapped): @@ -25,39 +26,62 @@ def log_migration(wrapped): return wrapped(*args, **kwargs) return wrapper +@log_migration +def init_rbac_migration(apps, schema_editor): + set_current_apps(apps) + @log_migration def migrate_users(apps, schema_editor): User = apps.get_model('auth', "User") Role = apps.get_model('main', "Role") RolePermission = apps.get_model('main', "RolePermission") ContentType = apps.get_model('contenttypes', "ContentType") + user_content_type = ContentType.objects.get_for_model(User) for user in User.objects.iterator(): + user.save() try: - Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=user.id) + Role.objects.get(content_type=user_content_type, object_id=user.id) 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)), - content_object = user, + content_type = user_content_type, + object_id = user.id ) role.members.add(user) RolePermission.objects.create( + created=now(), + modified=now(), role = role, - resource = user, + content_type = user_content_type, + object_id = user.id, create=1, read=1, write=1, delete=1, update=1, execute=1, scm_update=1, use=1, ) logger.info(smart_text(u"migrating to new role for user: {}".format(user.username))) if user.is_superuser: - Role.singleton('System Administrator').members.add(user) + 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' + ) + + sa_role.members.add(user) logger.warning(smart_text(u"added superuser: {}".format(user.username))) @log_migration def migrate_organization(apps, schema_editor): Organization = apps.get_model('main', "Organization") for org in Organization.objects.iterator(): + org.save() # force creates missing roles for admin in org.deprecated_admins.all(): org.admin_role.members.add(admin) logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username))) @@ -69,6 +93,7 @@ def migrate_organization(apps, schema_editor): def migrate_team(apps, schema_editor): Team = apps.get_model('main', 'Team') for t in Team.objects.iterator(): + t.save() for user in t.deprecated_users.all(): t.member_role.members.add(user) logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username))) @@ -110,7 +135,7 @@ def _discover_credentials(instances, cred, orgfunc): orgs[orgfunc(inst)].append(inst) if len(orgs) == 1: - _update_credential_parents(instances[0].inventory.organization, cred) + _update_credential_parents(orgfunc(instances[0]), cred) else: for pos, org in enumerate(orgs): if pos == 0: @@ -135,9 +160,14 @@ def migrate_credential(apps, schema_editor): Credential = apps.get_model('main', "Credential") JobTemplate = apps.get_model('main', 'JobTemplate') Project = apps.get_model('main', 'Project') + Role = apps.get_model('main', 'Role') + User = apps.get_model('auth', 'User') InventorySource = apps.get_model('main', 'InventorySource') + ContentType = apps.get_model('contenttypes', "ContentType") + user_content_type = ContentType.objects.get_for_model(User) for cred in Credential.objects.iterator(): + cred.save() results = (JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all() or InventorySource.objects.filter(credential=cred).all()) if results: @@ -164,7 +194,8 @@ def migrate_credential(apps, schema_editor): cred.save() logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host))) elif cred.deprecated_user is not None: - cred.deprecated_user.admin_role.children.add(cred.owner_role) + user_admin_role = Role.objects.get(content_type=user_content_type, object_id=cred.deprecated_user.id) + user_admin_role.children.add(cred.owner_role) cred.deprecated_user, cred.deprecated_team = None, None cred.save() logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, ))) @@ -191,6 +222,7 @@ def migrate_inventory(apps, schema_editor): return None for inventory in Inventory.objects.iterator(): + inventory.save() for perm in Permission.objects.filter(inventory=inventory): role = None execrole = None @@ -238,16 +270,18 @@ def migrate_projects(apps, schema_editor): # Migrate projects to single organizations, duplicating as necessary for project in Project.objects.iterator(): + project.save() original_project_name = project.name project_orgs = project.deprecated_organizations.distinct().all() - if len(project_orgs) > 1: + if len(project_orgs) >= 1: first_org = None for org in project_orgs: if first_org is None: # For the first org, re-use our existing Project object, so don't do the below duplication effort first_org = org - project.name = smart_text(u'{} - {}'.format(first_org.name, original_project_name)) + if len(project_orgs) > 1: + project.name = smart_text(u'{} - {}'.format(first_org.name, original_project_name)) project.organization = first_org project.save() else: @@ -349,6 +383,7 @@ def migrate_job_templates(apps, schema_editor): Permission = apps.get_model('main', 'Permission') for jt in JobTemplate.objects.iterator(): + jt.save() permission = Permission.objects.filter( inventory=jt.inventory, project=jt.project, diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index a8b2b58210..91e055f6eb 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -83,6 +83,7 @@ class Role(CommonModelNameNotUnique): singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True) parents = models.ManyToManyField('Role', related_name='children') + implicit_parents = models.TextField(null=False, default='[]') ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list` members = models.ManyToManyField('auth.User', related_name='roles') content_type = models.ForeignKey(ContentType, null=True, default=None) diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index 4d45d0e446..6fae236667 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -91,6 +91,46 @@ def test_project_migration(): assert o2.projects.all()[0].jobtemplates.count() == 1 assert o3.projects.all()[0].jobtemplates.count() == 0 +@pytest.mark.django_db +def test_single_org_project_migration(organization): + project = Project.objects.create(name='my project', + description="description", + organization=None) + organization.deprecated_projects.add(project) + assert project.organization is None + rbac.migrate_projects(apps, None) + project = Project.objects.get(id=project.id) + assert project.organization.id == organization.id + +@pytest.mark.django_db +def test_no_org_project_migration(organization): + project = Project.objects.create(name='my project', + description="description", + organization=None) + assert project.organization is None + rbac.migrate_projects(apps, None) + assert project.organization is None + +@pytest.mark.django_db +def test_multi_org_project_migration(): + org1 = Organization.objects.create(name="org1", description="org1 desc") + org2 = Organization.objects.create(name="org2", description="org2 desc") + project = Project.objects.create(name='my project', + description="description", + organization=None) + + assert Project.objects.all().count() == 1 + assert Project.objects.filter(organization=org1).count() == 0 + assert Project.objects.filter(organization=org2).count() == 0 + + project.deprecated_organizations.add(org1) + project.deprecated_organizations.add(org2) + assert project.organization is None + rbac.migrate_projects(apps, None) + assert Project.objects.filter(organization=org1).count() == 1 + assert Project.objects.filter(organization=org2).count() == 1 + + @pytest.mark.django_db def test_project_user_project(user_project, project, user): u = user('owner') diff --git a/awx/main/utils.py b/awx/main/utils.py index dea2155597..f1d85f72b2 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -20,6 +20,7 @@ import tempfile from rest_framework.exceptions import ParseError, PermissionDenied from django.utils.encoding import smart_str from django.core.urlresolvers import reverse +from django.apps import apps # PyCrypto from Crypto.Cipher import AES @@ -30,7 +31,8 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'get_ansible_version', 'get_ssh_version', 'get_awx_version', 'update_scm_url', 'get_type_for_model', 'get_model_for_type', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', - '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided'] + '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', + 'get_current_apps', 'set_current_apps'] def get_object_or_400(klass, *args, **kwargs): @@ -556,3 +558,11 @@ def getattrd(obj, name, default=NoDefaultProvided): return default raise +current_apps = apps +def set_current_apps(apps): + global current_apps + current_apps = apps + +def get_current_apps(): + global current_apps + return current_apps diff --git a/pytest.ini b/pytest.ini index 3e3f145d85..6ce4c8e3d4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,7 +3,7 @@ DJANGO_SETTINGS_MODULE = awx.settings.development python_paths = awx/lib/site-packages site_dirs = awx/lib/site-packages python_files = *.py -addopts = --reuse-db +addopts = --reuse-db --nomigrations markers = ac: access control test license_feature: ensure license features are accessible or not depending on license