Initial (editable) pass of adding JT.organization

This is the old version of this feature from 2019
  this allows setting the organization in the data sent
  to the API when creating a JT, and exposes the field
  in the UI as well

Subsequent commit changes the field from editable
  to read-only, but as of this commit, the machinery
  is not hooked up to infer it from project
This commit is contained in:
AlanCoding
2020-01-16 14:59:43 -05:00
parent 1876849d89
commit daa9282790
46 changed files with 985 additions and 377 deletions

View File

@@ -0,0 +1,72 @@
# Generated by Django 2.2.4 on 2019-08-07 19:56
import awx.main.utils.polymorphic
import awx.main.fields
from django.db import migrations, models
import django.db.models.deletion
from awx.main.migrations._rbac import rebuild_role_parentage, migrate_ujt_organization, migrate_ujt_organization_backward
class Migration(migrations.Migration):
dependencies = [
('main', '0084_v360_token_description'),
]
operations = [
# backwards parents and ancestors caching
migrations.RunPython(migrations.RunPython.noop, rebuild_role_parentage),
# add new organization field for JT and all other unified jobs
migrations.AddField(
model_name='unifiedjob',
name='tmp_organization',
field=models.ForeignKey(blank=True, help_text='The organization used to determine access to this unified job.', null=True, on_delete=awx.main.utils.polymorphic.SET_NULL, related_name='unifiedjobs', to='main.Organization'),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='tmp_organization',
field=models.ForeignKey(blank=True, help_text='The organization used to determine access to this template.', null=True, on_delete=awx.main.utils.polymorphic.SET_NULL, related_name='unifiedjobtemplates', to='main.Organization'),
),
# while new and old fields exist, copy the organization fields
migrations.RunPython(migrate_ujt_organization, migrate_ujt_organization_backward),
# with data saved, remove old fields
migrations.RemoveField(
model_name='project',
name='organization',
),
migrations.RemoveField(
model_name='workflowjobtemplate',
name='organization',
),
# now, without safely rename the new field without conflicts from old field
migrations.RenameField(
model_name='unifiedjobtemplate',
old_name='tmp_organization',
new_name='organization',
),
migrations.RenameField(
model_name='unifiedjob',
old_name='tmp_organization',
new_name='organization',
),
# parentage of job template roles has genuinely changed at this point
migrations.AlterField(
model_name='jobtemplate',
name='admin_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['organization.job_template_admin_role'], related_name='+', to='main.Role'),
),
migrations.AlterField(
model_name='jobtemplate',
name='execute_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['admin_role', 'organization.execute_role'], related_name='+', to='main.Role'),
),
migrations.AlterField(
model_name='jobtemplate',
name='read_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['organization.auditor_role', 'inventory.organization.auditor_role', 'execute_role', 'admin_role'], related_name='+', to='main.Role'),
),
# Re-compute the role parents and ancestors caching
# this may be a no-op because field post_save hooks from migrate_jt_organization
migrations.RunPython(rebuild_role_parentage, migrations.RunPython.noop),
]

View File

@@ -1,6 +1,9 @@
import logging
from time import time
from django.db.models import Subquery, OuterRef
from awx.main.fields import update_role_parentage_for_instance
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
logger = logging.getLogger('rbac_migrations')
@@ -10,11 +13,11 @@ def create_roles(apps, schema_editor):
'''
Implicit role creation happens in our post_save hook for all of our
resources. Here we iterate through all of our resource types and call
.save() to ensure all that happens for every object in the system before we
get busy with the actual migration work.
.save() to ensure all that happens for every object in the system.
This gets run after migrate_users, which does role creation for users a
little differently.
This can be used whenever new roles are introduced in a migration to
create those roles for pre-existing objects that did not previously
have them created via signals.
'''
models = [
@@ -35,7 +38,118 @@ def create_roles(apps, schema_editor):
obj.save()
def delete_all_user_roles(apps, schema_editor):
ContentType = apps.get_model('contenttypes', "ContentType")
Role = apps.get_model('main', "Role")
User = apps.get_model('auth', "User")
user_content_type = ContentType.objects.get_for_model(User)
for role in Role.objects.filter(content_type=user_content_type).iterator():
role.delete()
UNIFIED_ORG_LOOKUPS = {
# Job Templates had an implicit organization via their project
'jobtemplate': 'project',
# Inventory Sources had an implicit organization via their inventory
'inventorysource': 'inventory',
# Projects had an explicit organization in their subclass table
'project': None,
# Workflow JTs also had an explicit organization in their subclass table
'workflowjobtemplate': None,
# Jobs inherited project from job templates as a convenience field
'job': 'project',
# Inventory Sources had an convenience field of inventory
'inventoryupdate': 'inventory',
# Project Updates did not have a direct organization field, obtained it from project
'projectupdate': 'project',
# Workflow Jobs are handled same as project updates
# Sliced jobs are a special case, but old data is not given special treatment for simplicity
'workflowjob': 'workflow_job_template',
# AdHocCommands do not have a template, but still migrate them
'adhoccommand': 'inventory'
}
def implicit_org_subquery(UnifiedClass, cls, backward=False):
"""Returns a subquery that returns the so-called organization for objects
in the class in question, before migration to the explicit unified org field.
In some cases, this can still be applied post-migration.
"""
if cls._meta.model_name not in UNIFIED_ORG_LOOKUPS:
return None
cls_name = cls._meta.model_name
source_field = UNIFIED_ORG_LOOKUPS[cls_name]
unified_field = UnifiedClass._meta.get_field(cls_name)
unified_ptr = unified_field.remote_field.name
if backward:
qs = UnifiedClass.objects.filter(**{cls_name: OuterRef('id')}).order_by().values_list('tmp_organization')[:1]
elif source_field is None:
qs = cls.objects.filter(**{unified_ptr: OuterRef('id')}).order_by().values_list('organization')[:1]
else:
intermediary_field = cls._meta.get_field(source_field)
intermediary_model = intermediary_field.related_model
intermediary_reverse_rel = intermediary_field.remote_field.name
qs = intermediary_model.objects.filter(**{
# this filter leverages the fact that the Unified models have same pk as subclasses.
# For instance... filters projects used in job template, where that job template
# has same id same as UJT from the outer reference (which it does)
intermediary_reverse_rel: OuterRef('id')}
).order_by().values_list('organization')[:1]
return Subquery(qs)
def _migrate_unified_organization(apps, unified_cls_name, backward=False):
"""Given a unified base model (either UJT or UJ)
and a dict org_field_mapping which gives related model to get org from
saves organization for those objects to the temporary migration
variable tmp_organization on the unified model
(optimized method)
"""
start = time()
UnifiedClass = apps.get_model('main', unified_cls_name)
ContentType = apps.get_model('contenttypes', 'ContentType')
for cls in UnifiedClass.__subclasses__():
cls_name = cls._meta.model_name
if backward and UNIFIED_ORG_LOOKUPS.get(cls_name, 'not-found') is not None:
logger.debug('Not reverse migrating {}, existing data should remain valid'.format(cls_name))
continue
logger.debug('Migrating {} to new organization field'.format(cls_name))
sub_qs = implicit_org_subquery(UnifiedClass, cls, backward=backward)
if sub_qs is None:
logger.debug('Class {} has no organization migration'.format(cls_name))
continue
this_ct = ContentType.objects.get_for_model(cls)
if backward:
r = cls.objects.order_by().update(organization=sub_qs)
else:
r = UnifiedClass.objects.order_by().filter(polymorphic_ctype=this_ct).update(tmp_organization=sub_qs)
if r:
logger.info('Organization migration on {} affected {} rows.'.format(cls_name, r))
logger.info('Unified organization migration completed in %f seconds' % (time() - start))
def migrate_ujt_organization(apps, schema_editor):
'''Move organization field to UJT and UJ models'''
_migrate_unified_organization(apps, 'UnifiedJobTemplate')
_migrate_unified_organization(apps, 'UnifiedJob')
def migrate_ujt_organization_backward(apps, schema_editor):
'''Move organization field from UJT and UJ models back to their original places'''
_migrate_unified_organization(apps, 'UnifiedJobTemplate', backward=True)
_migrate_unified_organization(apps, 'UnifiedJob', backward=True)
def rebuild_role_hierarchy(apps, schema_editor):
'''
This should be called in any migration when ownerships are changed.
Ex. I remove a user from the admin_role of a credential.
Ancestors are cached from parents for performance, this re-computes ancestors.
'''
logger.info('Computing role roots..')
start = time()
roots = Role.objects \
@@ -46,14 +160,57 @@ def rebuild_role_hierarchy(apps, schema_editor):
start = time()
Role.rebuild_role_ancestor_list(roots, [])
stop = time()
logger.info('Rebuild completed in %f seconds' % (stop - start))
logger.info('Rebuild ancestors completed in %f seconds' % (stop - start))
logger.info('Done.')
def delete_all_user_roles(apps, schema_editor):
ContentType = apps.get_model('contenttypes', "ContentType")
def rebuild_role_parentage(apps, schema_editor):
'''
This should be called in any migration when any parent_role entry
is modified so that the cached parent fields will be updated. Ex:
foo_role = ImplicitRoleField(
parent_role=['bar_role'] # change to parent_role=['admin_role']
)
This is like rebuild_role_hierarchy, but that method updates ancestors,
whereas this method updates parents.
'''
start = time()
seen_models = set()
updated_ct = 0
model_ct = 0
noop_ct = 0
Role = apps.get_model('main', "Role")
User = apps.get_model('auth', "User")
user_content_type = ContentType.objects.get_for_model(User)
for role in Role.objects.filter(content_type=user_content_type).iterator():
role.delete()
for role in Role.objects.iterator():
if not role.object_id:
noop_ct += 1
continue
model_tuple = (role.content_type_id, role.object_id)
if model_tuple in seen_models:
continue
seen_models.add(model_tuple)
# The GenericForeignKey does not work right in migrations
# with the usage as role.content_object
# so we do the lookup ourselves with current migration models
ct = role.content_type
app = ct.app_label
ct_model = apps.get_model(app, ct.model)
content_object = ct_model.objects.get(pk=role.object_id)
updated = update_role_parentage_for_instance(content_object)
if updated:
model_ct += 1
logger.debug('Updated parents of {} roles of {}'.format(updated, content_object))
else:
noop_ct += 1
updated_ct += updated
logger.debug('No changes to role parents for {} roles'.format(noop_ct))
if updated_ct:
logger.info('Updated parentage for {} roles of {} resources'.format(updated_ct, model_ct))
logger.info('Rebuild parentage completed in %f seconds' % (time() - start))
if updated_ct:
rebuild_role_hierarchy(apps, schema_editor)