diff --git a/awx/main/migrations/0007_v300_rbac_changes.py b/awx/main/migrations/0007_v300_rbac_changes.py index 89252ca12c..3a8e3f615a 100644 --- a/awx/main/migrations/0007_v300_rbac_changes.py +++ b/awx/main/migrations/0007_v300_rbac_changes.py @@ -184,4 +184,19 @@ class Migration(migrations.Migration): name='rolepermission', index_together=set([('content_type', 'object_id')]), ), + migrations.RenameField( + model_name='organization', + old_name='projects', + new_name='deprecated_projects', + ), + migrations.AlterField( + model_name='organization', + 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), + ), ] diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 4cdb1bc168..bbf3e5ada8 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -1,3 +1,5 @@ +from django.db import connection, transaction, reset_queries +from django.db.transaction import TransactionManagementError from django.contrib.contenttypes.models import ContentType from collections import defaultdict @@ -131,31 +133,72 @@ def migrate_projects(apps, schema_editor): Project = apps.get_model('main', 'Project') Permission = apps.get_model('main', 'Permission') + JobTemplate = apps.get_model('main', 'JobTemplate') - for project in Project.objects.all(): - if project.organizations.count() == 0 and project.created_by is not None: + # Migrate projects to single organizations, duplicating as necessary + for project in [p for p in Project.objects.all()]: + original_project_name = project.name + project_orgs = project.deprecated_organizations.distinct().all() + + if project_orgs.count() > 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 = first_org.name + ' - ' + original_project_name + project.organization = first_org + project.save() + else: + print('Fork to %s ' % (org.name + ' - ' + original_project_name)) + new_prj = Project.objects.create( + created = project.created, + description = project.description, + name = org.name + ' - ' + original_project_name, + old_pk = project.old_pk, + created_by_id = project.created_by_id, + scm_type = project.scm_type, + scm_url = project.scm_url, + scm_branch = project.scm_branch, + scm_clean = project.scm_clean, + scm_delete_on_update = project.scm_delete_on_update, + scm_delete_on_next_update = project.scm_delete_on_next_update, + scm_update_on_launch = project.scm_update_on_launch, + scm_update_cache_timeout = project.scm_update_cache_timeout, + credential = project.credential, + organization = org + ) + migrations[original_project_name]['projects'].add(new_prj) + job_templates = JobTemplate.objects.filter(inventory__organization=org).all() + for jt in job_templates: + jt.project = new_prj + print('Updating jt to point to %s' % repr(new_prj)) + jt.save() + + # Migrate permissions + for project in [p for p in Project.objects.all()]: + if project.organization is not None and project.created_by is not None: project.admin_role.members.add(project.created_by) - migrations[project.name]['users'].add(project.created_by) + migrations[original_project_name]['users'].add(project.created_by) for team in project.teams.all(): team.member_role.children.add(project.member_role) - migrations[project.name]['teams'].add(team) + migrations[original_project_name]['teams'].add(team) - if project.organizations.count() > 0: - for org in project.organizations.all(): - for user in org.users.all(): - project.member_role.members.add(user) - migrations[project.name]['users'].add(user) + if project.organization is not None: + for user in project.organization.member_role.members.all(): + project.member_role.members.add(user) + migrations[original_project_name]['users'].add(user) for perm in Permission.objects.filter(project=project, active=True): # All perms at this level just imply a user or team can read if perm.team: perm.team.member_role.children.add(project.member_role) - migrations[project.name]['teams'].add(perm.team) + migrations[original_project_name]['teams'].add(perm.team) if perm.user: project.member_role.members.add(perm.user) - migrations[project.name]['users'].add(perm.user) + migrations[original_project_name]['users'].add(perm.user) return migrations diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 3f86d5032a..af2817b326 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -38,10 +38,10 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): app_label = 'main' ordering = ('name',) - projects = models.ManyToManyField( + deprecated_projects = models.ManyToManyField( 'Project', blank=True, - related_name='organizations', + related_name='deprecated_organizations', ) admin_role = ImplicitRoleField( role_name='Organization Administrator', diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 4bb66c24d6..bcf54627d1 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -198,6 +198,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): app_label = 'main' ordering = ('id',) + organization = models.ForeignKey( + 'Organization', + blank=True, + null=True, + on_delete=models.CASCADE, + related_name='projects', + ) scm_delete_on_next_update = models.BooleanField( default=False, editable=False, @@ -212,13 +219,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): admin_role = ImplicitRoleField( role_name='Project Administrator', role_description='May manage this project', - parent_role='organizations.admin_role', + parent_role='organization.admin_role', permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Project Auditor', role_description='May read all settings associated with this project', - parent_role='organizations.auditor_role', + parent_role='organization.auditor_role', permissions = {'read': True} ) member_role = ImplicitRoleField( diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 01c2f000f3..c2abe4ffd5 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -97,8 +97,9 @@ def project(instance, organization): prj = Project.objects.create(name="test-proj", description="test-proj-desc", scm_type="git", - scm_url="https://github.com/jlaska/ansible-playbooks") - prj.organizations.add(organization) + scm_url="https://github.com/jlaska/ansible-playbooks", + organization=organization + ) return prj @pytest.fixture diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py index ffa6027f73..dd85ffcf4b 100644 --- a/awx/main/tests/functional/test_notifications.py +++ b/awx/main/tests/functional/test_notifications.py @@ -69,7 +69,7 @@ def test_encrypted_subfields(get, post, user, organization): assert response.data['notification_configuration']['account_token'] == "$encrypted$" with mock.patch.object(notifier_actual.notification_class, "send_messages", assert_send): notifier_actual.send("Test", {'body': "Test"}) - + @pytest.mark.django_db def test_inherited_notifiers(get, post, user, organization, project): u = user('admin-poster', True) @@ -86,7 +86,6 @@ def test_inherited_notifiers(get, post, user, organization, project): u) assert response.status_code == 201 notifiers.append(response.data['id']) - organization.projects.add(project) i = Inventory.objects.create(name='test', organization=organization) i.save() g = Group.objects.create(name='test', inventory=i) @@ -109,7 +108,6 @@ def test_inherited_notifiers(get, post, user, organization, project): @pytest.mark.django_db def test_notifier_merging(get, post, user, organization, project, notifier): user('admin-poster', True) - organization.projects.add(project) organization.notifiers_any.add(notifier) project.notifiers_any.add(notifier) assert len(project.notifiers['any']) == 1 diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index 64bb3437bd..d806e5e24c 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -1,11 +1,96 @@ import pytest from awx.main.migrations import _rbac as rbac -from awx.main.models import Role +from awx.main.models import Role, Permission, Project, Organization, Credential, JobTemplate, Inventory from django.apps import apps from awx.main.migrations import _old_access as old_access +@pytest.mark.django_db +def test_project_migration(): + ''' + + o1 o2 o3 with o1 -- i1 o2 -- i2 + \ | / + \ | / + c1 ---- p1 + / | \ + / | \ + jt1 jt2 jt3 + | | | + i1 i2 i1 + + + goes to + + + o1 + | + | + c1 ---- p1 + / | + / | + jt1 jt3 + | | + i1 i1 + + + o2 + | + | + c1 ---- p2 + | + | + jt2 + | + i2 + + o3 + | + | + c1 ---- p3 + + + ''' + + + o1 = Organization.objects.create(name='o1') + o2 = Organization.objects.create(name='o2') + o3 = Organization.objects.create(name='o3') + + c1 = Credential.objects.create(name='c1') + + p1 = Project.objects.create(name='p1', credential=c1) + p1.deprecated_organizations.add(o1, o2, o3) + + i1 = Inventory.objects.create(name='i1', organization=o1) + i2 = Inventory.objects.create(name='i2', organization=o2) + + jt1 = JobTemplate.objects.create(name='jt1', project=p1, inventory=i1) + jt2 = JobTemplate.objects.create(name='jt2', project=p1, inventory=i2) + jt3 = JobTemplate.objects.create(name='jt3', project=p1, inventory=i1) + + assert o1.projects.count() == 0 + assert o2.projects.count() == 0 + assert o3.projects.count() == 0 + + rbac.migrate_projects(apps, None) + + jt1 = JobTemplate.objects.get(pk=jt1.pk) + jt2 = JobTemplate.objects.get(pk=jt2.pk) + jt3 = JobTemplate.objects.get(pk=jt3.pk) + + assert jt1.project == jt3.project + assert jt1.project != jt2.project + + assert o1.projects.count() == 1 + assert o2.projects.count() == 1 + assert o3.projects.count() == 1 + assert o1.projects.all()[0].jobtemplates.count() == 2 + assert o2.projects.all()[0].jobtemplates.count() == 1 + assert o3.projects.all()[0].jobtemplates.count() == 0 + + #@pytest.mark.django_db #def test_project_user_project(user_project, project, user): # u = user('owner') @@ -56,13 +141,14 @@ from awx.main.migrations import _old_access as old_access # assert len(migrations[project.name]['teams']) == 0 # assert project.accessible_by(admin, {'read': True, 'write': True}) is True # assert project.accessible_by(member, {'read': True}) is False - +# #@pytest.mark.django_db #def test_project_team(user, team, project): # nonmember = user('nonmember') # member = user('member') # -# team.users.add(member) +# #team.users.add(member) +# team.member_role.members.add(member) # project.teams.add(team) # # assert project.accessible_by(nonmember, {'read': True}) is False @@ -76,14 +162,14 @@ from awx.main.migrations import _old_access as old_access # assert len(migrations[project.name]['teams']) == 1 # assert project.accessible_by(member, {'read': True}) is True # assert project.accessible_by(nonmember, {'read': True}) is False - +# #@pytest.mark.django_db #def test_project_explicit_permission(user, team, project, organization): # u = user('prjuser') # # assert old_access.check_user_access(u, project.__class__, 'read', project) is False # -# organization.users.add(u) +# organization.member_role.members.add(u) # p = Permission(user=u, project=project, permission_type='create', name='Perm name') # p.save() #