From f7dc3c0f0d4b004a6040568bf585cffe08b2d582 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 15:22:00 -0500 Subject: [PATCH 1/6] Added an explicit member role, distinct from auditor role --- awx/main/models/organization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 5b24b304bd..2d61fc81e6 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -62,6 +62,11 @@ class Organization(CommonModel, ResourceMixin): resource_field='resource', permissions = {'read': True} ) + member_role = ImplicitRoleField( + role_name='Organization Member', + resource_field='resource', + permissions = {'read': True} + ) def get_absolute_url(self): From 5008e3faf5621d6490e35b969aadef91103794ed Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 22:44:41 -0500 Subject: [PATCH 2/6] Add parent System roles to organization roles --- awx/main/models/organization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 2d61fc81e6..2648784236 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -54,11 +54,13 @@ class Organization(CommonModel, ResourceMixin): ) admin_role = ImplicitRoleField( role_name='Organization Administrator', + parent_role='singleton:System Administrator', resource_field='resource', permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Organization Auditor', + parent_role='singleton:System Auditor', resource_field='resource', permissions = {'read': True} ) From d51447e15839e448f10b24306cac871663d8dce6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 22:45:57 -0500 Subject: [PATCH 3/6] Migration and tests for super users --- awx/main/migrations/_rbac.py | 10 ++++++++++ awx/main/tests/functional/test_rbac_user.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 awx/main/tests/functional/test_rbac_user.py diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index d2bddc8302..9b97c5a80a 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -1,5 +1,15 @@ from collections import defaultdict +def migrate_users(apps, schema_editor): + migrations = list() + User = apps.get_model('auth', "User") + Role = apps.get_model('main', "Role") + for user in User.objects.all(): + if user.is_superuser: + Role.singleton('System Administrator').members.add(user) + migrations.append(user) + return migrations + def migrate_organization(apps, schema_editor): migrations = defaultdict(list) organization = apps.get_model('main', "Organization") diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py new file mode 100644 index 0000000000..f670b26220 --- /dev/null +++ b/awx/main/tests/functional/test_rbac_user.py @@ -0,0 +1,20 @@ +import pytest + +from awx.main.migrations import _rbac as rbac +from awx.main.models import Role +from django.apps import apps + +@pytest.mark.django_db +def test_user_admin(user_project, project, user): + joe = user('joe', is_superuser = False) + admin = user('admin', is_superuser = True) + sa = Role.singleton('System Administrator') + + assert sa.members.filter(id=joe.id).exists() is False + assert sa.members.filter(id=admin.id).exists() is False + + migrations = rbac.migrate_users(apps, None) + + assert sa.members.filter(id=joe.id).exists() is False + assert sa.members.filter(id=admin.id).exists() is True + assert len(migrations) == 1 From 34067d9c0e63120cc1886bf3da976393c2601bfa Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 22:49:10 -0500 Subject: [PATCH 4/6] Project migration and tests --- awx/main/migrations/_rbac.py | 46 +++++++++++ awx/main/models/projects.py | 5 +- awx/main/tests/functional/conftest.py | 10 +++ .../tests/functional/test_rbac_project.py | 79 +++++++++++++++++++ 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 awx/main/tests/functional/test_rbac_project.py diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 9b97c5a80a..76e4f83336 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -86,3 +86,49 @@ def migrate_inventory(apps, schema_editor): migrations[inventory.name]['teams'] = teams migrations[inventory.name]['users'] = users return migrations + +def migrate_projects(apps, schema_editor): + ''' + I can see projects when: + X I am a superuser. + X I am an admin in an organization associated with the project. + X I am a user in an organization associated with the project. + X I am on a team associated with the project. + X I have been explicitly granted permission to run/check jobs using the + project. + X I created the project but it isn't associated with an organization + I can change/delete when: + X I am a superuser. + X I am an admin in an organization associated with the project. + X I created the project but it isn't associated with an organization + ''' + migrations = defaultdict(lambda: defaultdict(set)) + + Project = apps.get_model('main', 'Project') + Permission = apps.get_model('main', 'Permission') + + for project in Project.objects.all(): + if project.organization is None and project.created_by is not None: + project.admin_role.members.add(project.created_by) + migrations[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) + + if project.organization is not None: + for user in project.organization.users.all(): + project.member_role.members.add(user) + migrations[project.name]['users'].add(user) + + for perm in Permission.objects.filter(project=project): + # All perms at this level just imply a user or team can read + if perm.team: + team.member_role.children.add(project.member_role) + migrations[project.name]['teams'].add(team) + + if perm.user: + project.member_role.members.add(perm.user) + migrations[project.name]['users'].add(perm.user) + + return migrations diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 1da3d51961..593f3e40ca 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -229,13 +229,12 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): ) member_role = ImplicitRoleField( role_name='Project Member', - parent_role='admin', resource_field='resource', - permissions = {'usage': True} + permissions = {'read': True} ) scm_update_role = ImplicitRoleField( role_name='Project Updater', - parent_role='admin', + parent_role='admin_role', resource_field='resource', permissions = {'scm_update': True} ) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 31e6eebf6c..db4143f13d 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -2,6 +2,7 @@ import pytest from awx.main.models.credential import Credential from awx.main.models.inventory import Inventory +from awx.main.models.projects import Project from awx.main.models.organization import ( Organization, Team, @@ -23,6 +24,15 @@ def user(): def team(organization): return Team.objects.create(organization=organization, name='test-team') +@pytest.fixture +def project(organization): + return Project.objects.create(name="test-project", organization=organization, description="test-project-desc") + +@pytest.fixture +def user_project(user): + owner = user('owner') + return Project.objects.create(name="test-user-project", created_by=owner, description="test-user-project-desc") + @pytest.fixture def organization(): return Organization.objects.create(name="test-org", description="test-org-desc") diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py new file mode 100644 index 0000000000..95442036e4 --- /dev/null +++ b/awx/main/tests/functional/test_rbac_project.py @@ -0,0 +1,79 @@ +import pytest + +from awx.main.migrations import _rbac as rbac +from awx.main.models import Permission +from django.apps import apps + +@pytest.mark.django_db +def test_project_user_project(user_project, project, user): + u = user('owner') + assert user_project.accessible_by(u, {'read': True}) is False + assert project.accessible_by(u, {'read': True}) is False + migrations = rbac.migrate_projects(apps, None) + assert len(migrations[user_project.name]['users']) == 1 + assert len(migrations[user_project.name]['teams']) == 0 + assert user_project.accessible_by(u, {'read': True}) is True + assert project.accessible_by(u, {'read': True}) is False + +@pytest.mark.django_db +def test_project_accessible_by_sa(user, project): + u = user('systemadmin', is_superuser=True) + + assert project.accessible_by(u, {'read': True}) is False + su_migrations = rbac.migrate_users(apps, None) + migrations = rbac.migrate_projects(apps, None) + assert len(su_migrations) == 1 + assert len(migrations[project.name]['users']) == 0 + assert len(migrations[project.name]['teams']) == 0 + assert project.accessible_by(u, {'read': True, 'write': True}) is True + +@pytest.mark.django_db +def test_project_org_members(user, organization, project): + admin = user('orgadmin') + member = user('orgmember') + + assert project.accessible_by(admin, {'read': True}) is False + assert project.accessible_by(member, {'read': True}) is False + + organization.admin_role.members.add(admin) + organization.member_role.members.add(member) + + rbac.migrate_organization(apps, None) + migrations = rbac.migrate_projects(apps, None) + + assert len(migrations[project.name]['users']) == 0 + 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) + project.teams.add(team) + + assert project.accessible_by(nonmember, {'read': True}) is False + assert project.accessible_by(member, {'read': True}) is False + + rbac.migrate_team(apps, None) + migrations = rbac.migrate_projects(apps, None) + + assert len(migrations[project.name]['users']) == 0 + 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): + u = user('user') + p = Permission(user=u, project=project, permission_type='check') + p.save() + + assert project.accessible_by(u, {'read': True}) is False + + migrations = rbac.migrate_projects(apps, None) + + assert len(migrations[project.name]['users']) == 1 + assert project.accessible_by(u, {'read': True}) is True From a2b9777cc7ec4d606d3a33400c4f242bc9177fab Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 22:51:21 -0500 Subject: [PATCH 5/6] Add migrate_users and migrate_projects to our migration plan --- awx/main/migrations/0004_rbac_migrations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/migrations/0004_rbac_migrations.py b/awx/main/migrations/0004_rbac_migrations.py index 31bb92af98..62b90a6783 100644 --- a/awx/main/migrations/0004_rbac_migrations.py +++ b/awx/main/migrations/0004_rbac_migrations.py @@ -12,8 +12,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(rbac.migrate_users), migrations.RunPython(rbac.migrate_organization), migrations.RunPython(rbac.migrate_credential), migrations.RunPython(rbac.migrate_team), migrations.RunPython(rbac.migrate_inventory), + migrations.RunPython(rbac.migrate_projects), ] From a03d48eeb7bf9e6fcf4b6ec564d8c1868b6b867d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 8 Feb 2016 22:53:22 -0500 Subject: [PATCH 6/6] Add member_role to organizations --- awx/main/migrations/0003_rbac_changes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/migrations/0003_rbac_changes.py b/awx/main/migrations/0003_rbac_changes.py index f26aee850d..0b9de6c100 100644 --- a/awx/main/migrations/0003_rbac_changes.py +++ b/awx/main/migrations/0003_rbac_changes.py @@ -208,6 +208,11 @@ class Migration(migrations.Migration): name='resource', field=awx.main.fields.ImplicitResourceField(related_name='+', to='main.Resource', null=b'True'), ), + migrations.AddField( + model_name='organization', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', to='main.Role', null=b'True'), + ), migrations.AddField( model_name='project', name='admin_role',