1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 23:51:09 +03:00

allow adding teams to org object roles

This commit is contained in:
AlanCoding 2018-07-02 11:31:07 -04:00
parent 2dd3014374
commit e044b996e5
No known key found for this signature in database
GPG Key ID: FD2C3C012A72926B
9 changed files with 143 additions and 58 deletions

View File

@ -1204,8 +1204,8 @@ class TeamRolesList(SubListAttachDetachAPIView):
role = get_object_or_400(Role, pk=sub_id) role = get_object_or_400(Role, pk=sub_id)
org_content_type = ContentType.objects.get_for_model(Organization) org_content_type = ContentType.objects.get_for_model(Organization)
if role.content_type == org_content_type: if role.content_type == org_content_type and role.role_field in ['member_role', 'admin_role']:
data = dict(msg=_("You cannot assign an Organization role as a child role for a Team.")) data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team."))
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
if role.is_singleton(): if role.is_singleton():
@ -5071,8 +5071,8 @@ class RoleTeamsList(SubListAttachDetachAPIView):
role = Role.objects.get(pk=self.kwargs['pk']) role = Role.objects.get(pk=self.kwargs['pk'])
organization_content_type = ContentType.objects.get_for_model(Organization) organization_content_type = ContentType.objects.get_for_model(Organization)
if role.content_type == organization_content_type: if role.content_type == organization_content_type and role.role_field in ['member_role', 'admin_role']:
data = dict(msg=_("You cannot assign an Organization role as a child role for a Team.")) data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team."))
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
credential_content_type = ContentType.objects.get_for_model(Credential) credential_content_type = ContentType.objects.get_for_model(Credential)

View File

@ -5,6 +5,7 @@
import os import os
import sys import sys
import logging import logging
import six
# Django # Django
from django.conf import settings from django.conf import settings
@ -519,20 +520,24 @@ class UserAccess(BaseAccess):
return False return False
return bool(self.user == obj or self.can_admin(obj, data)) return bool(self.user == obj or self.can_admin(obj, data))
def user_membership_roles(self, u): @staticmethod
return Role.objects.filter( def user_organizations(u):
content_type=ContentType.objects.get_for_model(Organization), '''
role_field__in=Organization.member_role.field.parent_role + ['member_role'], Returns all organizations that count `u` as a member
members=u '''
) return Organization.accessible_objects(u, 'member_role')
def is_all_org_admin(self, u): def is_all_org_admin(self, u):
return not self.user_membership_roles(u).exclude( '''
ancestors__in=self.user.roles.filter(role_field='admin_role') returns True if `u` is member of any organization that is
not also an organization that `self.user` admins
'''
return not self.user_organizations(u).exclude(
pk__in=Organization.accessible_pk_qs(self.user, 'admin_role')
).exists() ).exists()
def user_is_orphaned(self, u): def user_is_orphaned(self, u):
return not self.user_membership_roles(u).exists() return not self.user_organizations(u).exists()
@check_superuser @check_superuser
def can_admin(self, obj, data, allow_orphans=False, check_setting=True): def can_admin(self, obj, data, allow_orphans=False, check_setting=True):
@ -2550,6 +2555,9 @@ class RoleAccess(BaseAccess):
# to admin the user being added to the role. # to admin the user being added to the role.
if (isinstance(obj.content_object, Organization) and if (isinstance(obj.content_object, Organization) and
obj.role_field in (Organization.member_role.field.parent_role + ['member_role'])): obj.role_field in (Organization.member_role.field.parent_role + ['member_role'])):
if not isinstance(sub_obj, User):
logger.error(six.text_type('Unexpected attempt to associate {} with organization role.').format(sub_obj))
return False
if not UserAccess(self.user).can_admin(sub_obj, None, allow_orphans=True): if not UserAccess(self.user).can_admin(sub_obj, None, allow_orphans=True):
return False return False

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-07-02 13:47
from __future__ import unicode_literals
import awx.main.fields
from django.db import migrations
import django.db.models.deletion
from awx.main.migrations._rbac import rebuild_role_hierarchy
class Migration(migrations.Migration):
dependencies = [
('main', '0041_v330_update_oauth_refreshtoken'),
]
operations = [
migrations.AlterField(
model_name='organization',
name='member_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'admin_role'], related_name='+', to='main.Role'),
),
migrations.AlterField(
model_name='organization',
name='read_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null=b'True', on_delete=django.db.models.deletion.CASCADE, parent_role=[b'member_role', b'auditor_role', b'execute_role', b'project_admin_role', b'inventory_admin_role', b'workflow_admin_role', b'notification_admin_role', b'credential_admin_role', b'job_template_admin_role'], related_name='+', to='main.Role'),
),
migrations.RunPython(rebuild_role_hierarchy),
]

View File

@ -67,13 +67,14 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
) )
member_role = ImplicitRoleField( member_role = ImplicitRoleField(
parent_role=['admin_role', 'execute_role', 'project_admin_role', parent_role=['admin_role']
'inventory_admin_role', 'workflow_admin_role',
'notification_admin_role', 'credential_admin_role',
'job_template_admin_role',]
) )
read_role = ImplicitRoleField( read_role = ImplicitRoleField(
parent_role=['member_role', 'auditor_role'], parent_role=['member_role', 'auditor_role',
'execute_role', 'project_admin_role',
'inventory_admin_role', 'workflow_admin_role',
'notification_admin_role', 'credential_admin_role',
'job_template_admin_role',],
) )

View File

@ -12,3 +12,28 @@ def test_admin_visible_to_orphaned_users(get, alice):
names.add(item['name']) names.add(item['name'])
assert 'System Auditor' in names assert 'System Auditor' in names
assert 'System Administrator' in names assert 'System Administrator' in names
@pytest.mark.django_db
@pytest.mark.parametrize('role,code', [
('member_role', 400),
('admin_role', 400),
('inventory_admin_role', 204)
])
@pytest.mark.parametrize('reversed', [
True, False
])
def test_org_object_role_assigned_to_team(post, team, organization, org_admin, role, code, reversed):
if reversed:
url = reverse('api:role_teams_list', kwargs={'pk': getattr(organization, role).id})
sub_id = team.id
else:
url = reverse('api:team_roles_list', kwargs={'pk': team.id})
sub_id = getattr(organization, role).id
post(
url=url,
data={'id': sub_id},
user=org_admin,
expect=code
)

View File

@ -684,7 +684,7 @@ def job_template_labels(organization, job_template):
@pytest.fixture @pytest.fixture
def workflow_job_template(organization): def workflow_job_template(organization):
wjt = WorkflowJobTemplate(name='test-workflow_job_template') wjt = WorkflowJobTemplate(name='test-workflow_job_template', organization=organization)
wjt.save() wjt.save()
return wjt return wjt

View File

@ -67,10 +67,46 @@ def test_org_user_role_attach(user, organization, inventory):
role_access = RoleAccess(admin) role_access = RoleAccess(admin)
assert not role_access.can_attach(organization.member_role, nonmember, 'members', None) assert not role_access.can_attach(organization.member_role, nonmember, 'members', None)
assert not role_access.can_attach(organization.notification_admin_role, nonmember, 'members', None)
assert not role_access.can_attach(organization.admin_role, nonmember, 'members', None) assert not role_access.can_attach(organization.admin_role, nonmember, 'members', None)
# Permissions when adding users/teams to org special-purpose roles
@pytest.mark.django_db
def test_user_org_object_roles(organization, org_admin, org_member):
'''
Unlike admin & member roles, the special-purpose organization roles do not
confer any permissions related to user management,
Normal rules about role delegation should apply, only admin to org needed.
'''
assert RoleAccess(org_admin).can_attach(
organization.notification_admin_role, org_member, 'members', None
)
assert not RoleAccess(org_member).can_attach(
organization.notification_admin_role, org_member, 'members', None
)
@pytest.mark.django_db
def test_team_org_object_roles(organization, team, org_admin, org_member):
'''
the special-purpose organization roles are not ancestors of any
team roles, and can be delegated en masse through teams,
following normal admin rules
'''
assert RoleAccess(org_admin).can_attach(
organization.notification_admin_role, team, 'member_role.parents', {'id': 68}
)
# Obviously team admin isn't enough to assign organization roles to the team
team.admin_role.members.add(org_member)
assert not RoleAccess(org_member).can_attach(
organization.notification_admin_role, team, 'member_role.parents', {'id': 68}
)
# Cannot make a team member of an org
assert not RoleAccess(org_admin).can_attach(
organization.member_role, team, 'member_role.parents', {'id': 68}
)
# Singleton user editing restrictions # Singleton user editing restrictions
@pytest.mark.django_db @pytest.mark.django_db
def test_org_superuser_role_attach(admin_user, org_admin, organization): def test_org_superuser_role_attach(admin_user, org_admin, organization):

View File

@ -106,6 +106,30 @@ def test_team_admin_member_access(team, user, project):
assert len(Project.accessible_objects(u, 'use_role')) == 1 assert len(Project.accessible_objects(u, 'use_role')) == 1
@pytest.mark.django_db
def test_team_member_org_role_access_project(team, rando, project, organization):
team.member_role.members.add(rando)
assert rando not in project.read_role
team.member_role.children.add(organization.project_admin_role)
assert rando in project.admin_role
@pytest.mark.django_db
def test_team_member_org_role_access_workflow(team, rando, workflow_job_template, organization):
team.member_role.members.add(rando)
assert rando not in workflow_job_template.read_role
team.member_role.children.add(organization.workflow_admin_role)
assert rando in workflow_job_template.admin_role
@pytest.mark.django_db
def test_team_member_org_role_access_inventory(team, rando, inventory, organization):
team.member_role.members.add(rando)
assert rando not in inventory.read_role
team.member_role.children.add(organization.inventory_admin_role)
assert rando in inventory.admin_role
@pytest.mark.django_db @pytest.mark.django_db
def test_org_admin_team_access(organization, team, user, project): def test_org_admin_team_access(organization, team, user, project):
u = user('team_admin', False) u = user('team_admin', False)

View File

@ -1,38 +0,0 @@
import mock
from rest_framework.test import APIRequestFactory
from rest_framework.test import force_authenticate
from django.contrib.contenttypes.models import ContentType
from awx.api.views import (
TeamRolesList,
)
from awx.main.models import (
User,
Role,
)
def test_team_roles_list_post_org_roles():
with mock.patch('awx.api.views.get_object_or_400') as role_get, \
mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get:
role_mock = mock.MagicMock(spec=Role)
content_type_mock = mock.MagicMock(spec=ContentType)
role_mock.content_type = content_type_mock
role_get.return_value = role_mock
ct_get.return_value = content_type_mock
factory = APIRequestFactory()
view = TeamRolesList.as_view()
request = factory.post("/team/1/roles", {'id':1}, format="json")
force_authenticate(request, User(username="root", is_superuser=True))
response = view(request)
response.render()
assert response.status_code == 400
assert 'cannot assign' in response.content