From b1c568e4d9add463b622d532a5ce7ff794190e21 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Apr 2016 16:31:11 -0400 Subject: [PATCH 01/15] Fix invalid usage of content_object during migrations #1380 #1425 --- awx/main/migrations/_rbac.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 318c6d4667..981ab97e1f 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -31,15 +31,17 @@ def migrate_users(apps, schema_editor): 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(): 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( 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( From ec439126e0e871798cf7c9ea96794e2de5219a4a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Apr 2016 16:36:47 -0400 Subject: [PATCH 02/15] Added created/modified and another content_object fix in our migrations #1380 #1425 --- awx/main/migrations/_rbac.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 981ab97e1f..866877b46d 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 +import _old_access as old_access logger = logging.getLogger(__name__) def log_migration(wrapped): @@ -39,14 +40,19 @@ 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)), 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, ) From 274744b4c1ded954a417356c3ef736129b4586ba Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 7 Apr 2016 16:44:18 -0400 Subject: [PATCH 03/15] Replaced Role.singleton usage in migrations as it doesn't exist here apparently --- awx/main/migrations/_rbac.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 866877b46d..85f2d3fd7d 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -59,7 +59,17 @@ def migrate_users(apps, schema_editor): 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 From 682552d9b0d128285088fe8a62d40ed7803c0bf7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 8 Apr 2016 09:40:56 -0400 Subject: [PATCH 04/15] Added field deconstruct method so ImplicitRoleField works in migrations Apparently we need this, who'da known. https://docs.djangoproject.com/en/1.9/howto/custom-model-fields/ --- awx/main/fields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awx/main/fields.py b/awx/main/fields.py index 1102ce4238..650edb55e0 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -97,6 +97,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)) From 7d2e66074981770d27d08901286993f5e292bdb3 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 10 Apr 2016 11:57:39 -0400 Subject: [PATCH 05/15] Make use of 'current' apps so RBAC ImplicitRoleField can work during migrations While a migration is taking place, we can't juse use normal model references like Role and RolePermission, nor can we use generic foreign keys without manually referring to the content type and object id fields. --- awx/main/fields.py | 42 +++++++++++++++---- .../migrations/0009_v300_rbac_migrations.py | 1 + awx/main/migrations/_rbac.py | 6 ++- awx/main/utils.py | 12 +++++- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 650edb55e0..90f94b734c 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -17,10 +17,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 Role, batch_role_ancestor_rebuilding +from awx.main.utils import get_current_apps __all__ = ['AutoOneToOneField', 'ImplicitRoleField'] @@ -65,8 +66,12 @@ 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: + Role_ = get_current_apps().get_model('main', 'Role') + 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) else: @@ -174,7 +179,10 @@ class ImplicitRoleField(models.ForeignKey): role = getattr(instance, self.name, None) if role: return role - role = Role.objects.create( + Role_ = get_current_apps().get_model('main', 'Role') + role = Role_.objects.create( + created=now(), + modified=now(), name=self.role_name, description=self.role_description ) @@ -186,9 +194,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 ) @@ -253,7 +268,20 @@ 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] else: parents = resolve_role_field(instance, path) for parent in parents: 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 85f2d3fd7d..de09d06fd1 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -5,7 +5,7 @@ from django.db.models import Q from django.utils.timezone import now from collections import defaultdict -from awx.main.utils import getattrd +from awx.main.utils import getattrd, set_current_apps import _old_access as old_access logger = logging.getLogger(__name__) @@ -26,6 +26,10 @@ 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") 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 From e3a45235a6f92be572059d5864e937a9f8102f23 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 10 Apr 2016 12:00:23 -0400 Subject: [PATCH 06/15] Do an initial save on all resources during amigration to force the creation of RBAC role objects. --- awx/main/migrations/_rbac.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index de09d06fd1..327542502c 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -39,6 +39,7 @@ def migrate_users(apps, schema_editor): user_content_type = ContentType.objects.get_for_model(User) for user in User.objects.iterator(): + user.save() try: 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))) @@ -80,6 +81,7 @@ def migrate_users(apps, schema_editor): 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))) @@ -91,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))) @@ -160,6 +163,7 @@ def migrate_credential(apps, schema_editor): InventorySource = apps.get_model('main', 'InventorySource') 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: @@ -213,6 +217,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 @@ -260,6 +265,7 @@ 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() @@ -371,6 +377,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, From 59b6ed3b9c263880eac5294eab09b4a4a0e121b2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 10 Apr 2016 12:02:06 -0400 Subject: [PATCH 07/15] Update rbac schema migrations so we have all parameters we need to operate during a migration We need all those rbac fields definied in the migration in order for the current apps to have access to them during a migration. --- awx/main/migrations/0008_v300_rbac_changes.py | 95 ++++++++++--------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 30d058cfd3..772a56a651 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,11 @@ class Migration(migrations.Migration): 'projects', 'deprecated_projects', ), + migrations.AlterField( + model_name='team', + name='deprecated_projects', + field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True), + ), migrations.CreateModel( name='Role', @@ -86,141 +90,144 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'permissions', }, ), - 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'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='+', 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'), - ), - 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 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='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')]), From 86e29221d5c3ec97fb33617a6836b0e9083efc59 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 10 Apr 2016 12:05:23 -0400 Subject: [PATCH 08/15] Fixed missing `orgfunc` usage to resolve organization during credential migration --- awx/main/migrations/_rbac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 327542502c..3b411094f4 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -135,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: From 5f8e7bbf5539b6c2f5ef410cbccf84075074f4cf Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 11:13:38 -0400 Subject: [PATCH 09/15] Replace user.admin_role usage with manual version that works in migrations --- awx/main/migrations/_rbac.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 3b411094f4..0c78c32495 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -160,7 +160,11 @@ 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() @@ -190,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, ))) From 74d86859f15e03fa55c6be01621c686f9a795200 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 11:44:17 -0400 Subject: [PATCH 10/15] Fix project migration when project has exactly one org --- awx/main/migrations/_rbac.py | 5 ++- .../tests/functional/test_rbac_project.py | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 0c78c32495..32c399df81 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -274,13 +274,14 @@ def migrate_projects(apps, schema_editor): 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: 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') From e9aff8d67d0a7070af81f4e894410d1ea8c27716 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 11 Apr 2016 13:39:44 -0400 Subject: [PATCH 11/15] Moved necessary add field migration to exist prior to dependent implicit role field which references the field --- awx/main/migrations/0008_v300_rbac_changes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 772a56a651..75934a69ed 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -37,6 +37,11 @@ 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', @@ -242,11 +247,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', From b77d8dab474e7a40ed7ffa5df7e609ec4fd37be5 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 12 Apr 2016 09:14:38 -0400 Subject: [PATCH 12/15] flake8 fixes Signed-off-by: Akita Noek --- awx/main/fields.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 90f94b734c..44aba25ccd 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -20,7 +20,7 @@ from django.utils.encoding import smart_text from django.utils.timezone import now # AWX -from awx.main.models.rbac import 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'] @@ -274,13 +274,11 @@ class ImplicitRoleField(models.ForeignKey): 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 - ) + role = Role_.objects.create(created=now(), + modified=now(), + singleton_name=singleton_name, + name=singleton_name, + description=singleton_name) parents = [role] else: parents = resolve_role_field(instance, path) From 7e0bfc9831e880f89f2abc62567394db978660ed Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 12 Apr 2016 14:32:10 -0400 Subject: [PATCH 13/15] Use --nomigrations for py.test This is for two reasons, 1) it's a lot faster when starting from a new database, and 2) since we do database work within our migrations, it doesn't actually work within py.test, which is either a bug in pytest-djano, or a horrible behavior of pytest-django. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7b4e7ec5b328d529178fbeb423a73ba5075da136 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 12 Apr 2016 11:39:14 -0400 Subject: [PATCH 14/15] Switch to explicitly stored implicit role parents Completes #1496 --- awx/main/fields.py | 42 +++++++------------ awx/main/migrations/0008_v300_rbac_changes.py | 1 + awx/main/models/rbac.py | 1 + 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 44aba25ccd..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, @@ -71,9 +73,9 @@ def resolve_role_field(obj, field): if len(field_components) == 1: Role_ = get_current_apps().get_model('main', 'Role') - 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) + 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(): @@ -178,7 +180,7 @@ class ImplicitRoleField(models.ForeignKey): def _create_role_instance_if_not_exists(self, instance): role = getattr(instance, self.name, None) if role: - return role + return Role_ = get_current_apps().get_model('main', 'Role') role = Role_.objects.create( created=now(), @@ -226,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): @@ -279,7 +267,7 @@ class ImplicitRoleField(models.ForeignKey): singleton_name=singleton_name, name=singleton_name, description=singleton_name) - parents = [role] + 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 75934a69ed..1ec3432c9e 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -64,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={ 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) From ad89491a07d32c13659ba58eb09363bc3921fe64 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 12 Apr 2016 16:31:11 -0400 Subject: [PATCH 15/15] Fixed bad merge --- awx/main/migrations/0008_v300_rbac_changes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index e6c9e40582..8848bbabd7 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -170,7 +170,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='inventory', 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 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(