diff --git a/awx/api/views.py b/awx/api/views.py index 1cb44f3c60..3fd803369c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3783,22 +3783,22 @@ class RoleTeamsList(ListAPIView): if not sub_id: data = dict(msg="Role 'id' field is missing.") return Response(data, status=status.HTTP_400_BAD_REQUEST) - # XXX: Need to pull in can_attach and can_unattach kinda code from SubListCreateAttachDetachAPIView + role = Role.objects.get(pk=self.kwargs['pk']) team = Team.objects.get(pk=sub_id) - from awx.main.access import RoleAccess - access = RoleAccess(request.user) - if access.can_attach(role, team, 'members', {"id": role.pk}, skip_sub_obj_read_check=False): + action = 'attach' + if request.data.get('disassociate', None): + action = 'unattach' + if not request.user.can_access(self.parent_model, action, role, team, + self.relationship, request.data, + skip_sub_obj_read_check=False): raise PermissionDenied() - if request.data.get('disassociate', None): team.member_role.children.remove(role) else: team.member_role.children.add(role) return Response(status=status.HTTP_204_NO_CONTENT) - # XXX attach/detach needs to ensure we have the appropriate perms - class RoleParentsList(SubListAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index eecaf69253..2a4f4746b0 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -270,11 +270,18 @@ class UserAccess(BaseAccess): return True return False - def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): + def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): + "Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment." if relationship == 'roles': role_access = RoleAccess(self.user) - return role_access.can_attach(sub_obj, obj, 'members', data, skip_sub_obj_read_check=False) - return super(UserAccess, self).can_attach(obj, sub_obj, relationship, data, skip_sub_obj_read_check=False) + return role_access.can_attach(sub_obj, obj, 'members', *args, **kwargs) + return super(UserAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs) + + def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): + if relationship == 'roles': + role_access = RoleAccess(self.user) + return role_access.can_unattach(sub_obj, obj, 'members', *args, **kwargs) + return super(UserAccess, self).can_unattach(obj, sub_obj, relationship, *args, **kwargs) class OrganizationAccess(BaseAccess): @@ -652,6 +659,23 @@ class TeamAccess(BaseAccess): def can_delete(self, obj): return self.can_change(obj, None) + def can_attach(self, obj, sub_obj, relationship, *args, **kwargs): + "Reverse obj and sub_obj, defer to RoleAccess if this is a role assignment." + if relationship == 'member_role.children': + role_access = RoleAccess(self.user) + return role_access.can_attach(sub_obj, obj, 'member_role.parents', + *args, **kwargs) + return super(TeamAccess, self).can_attach(obj, sub_obj, relationship, + *args, **kwargs) + + def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): + if relationship == 'member_role.children': + role_access = RoleAccess(self.user) + return role_access.can_unattach(sub_obj, obj, 'member_role.parents', + *args, **kwargs) + return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship, + *args, **kwargs) + class ProjectAccess(BaseAccess): ''' I can see projects when: diff --git a/awx/main/tests/functional/api/test_create_attach_views.py b/awx/main/tests/functional/api/test_create_attach_views.py new file mode 100644 index 0000000000..4882b8563a --- /dev/null +++ b/awx/main/tests/functional/api/test_create_attach_views.py @@ -0,0 +1,48 @@ +import pytest + +from django.core.urlresolvers import reverse + + +@pytest.mark.django_db +def test_user_role_view_access(rando, inventory, mocker, post): + "Assure correct access method is called when assigning users new roles" + role_pk = inventory.admin_role.pk + data = {"id": role_pk} + mock_access = mocker.MagicMock(can_attach=mocker.MagicMock(return_value=False)) + with mocker.patch('awx.main.access.RoleAccess', return_value=mock_access): + post(url=reverse('api:user_roles_list', args=(rando.pk,)), + data=data, user=rando, expect=403) + mock_access.can_attach.assert_called_once_with( + inventory.admin_role, rando, 'members', data, + skip_sub_obj_read_check=False) + assert rando not in inventory.admin_role + +@pytest.mark.django_db +def test_team_role_view_access(rando, team, inventory, mocker, post): + "Assure correct access method is called when assigning teams new roles" + team.admin_role.members.add(rando) + role_pk = inventory.admin_role.pk + data = {"id": role_pk} + mock_access = mocker.MagicMock(can_attach=mocker.MagicMock(return_value=False)) + with mocker.patch('awx.main.access.RoleAccess', return_value=mock_access): + post(url=reverse('api:team_roles_list', args=(team.pk,)), + data=data, user=rando, expect=403) + mock_access.can_attach.assert_called_once_with( + inventory.admin_role, team, 'member_role.parents', data, + skip_sub_obj_read_check=False) + assert team not in inventory.admin_role + +@pytest.mark.django_db +def test_role_team_view_access(rando, team, inventory, mocker, post): + """Assure that /role/N/teams/ enforces the same permission restrictions + that /teams/N/roles/ does when assigning teams new roles""" + role_pk = inventory.admin_role.pk + data = {"id": team.pk} + mock_access = mocker.MagicMock(return_value=False, __name__='mocked') + with mocker.patch('awx.main.access.RoleAccess.can_attach', mock_access): + post(url=reverse('api:role_teams_list', args=(role_pk,)), + data=data, user=rando, expect=403) + mock_access.assert_called_once_with( + inventory.admin_role, team, 'member_role.parents', data, + skip_sub_obj_read_check=False) + assert team not in inventory.admin_role diff --git a/awx/main/tests/functional/test_rbac_role.py b/awx/main/tests/functional/test_rbac_role.py index 20ab8ed6ba..c180efc198 100644 --- a/awx/main/tests/functional/test_rbac_role.py +++ b/awx/main/tests/functional/test_rbac_role.py @@ -1,64 +1,32 @@ -import mock import pytest from awx.main.access import ( RoleAccess, - UserAccess -) - -from django.core.urlresolvers import reverse -from django.contrib.auth.models import User + UserAccess, + TeamAccess) @pytest.mark.django_db -def test_user_role_access_view(rando, inventory, mocker, post): - # rando has read access for the inventory - inventory.read_role.members.add(rando) - - role_pk = inventory.admin_role.pk - mock_access = mocker.MagicMock(spec=RoleAccess, can_attach=mock.MagicMock(return_value=False)) - with mocker.patch('awx.main.access.RoleAccess', return_value=mock_access): - response = post(url=reverse('api:user_roles_list', args=(rando.pk,)), - data={'id': role_pk}, user=rando) - mock_access.can_attach.assert_called_once_with( - inventory.admin_role, rando, 'members', {"id": role_pk}, - skip_sub_obj_read_check=False) - assert rando not in inventory.admin_role - -@pytest.mark.django_db -def test_role_team_access_view(rando, team, inventory, mocker, post): +def test_team_access_attach(rando, team, inventory): # rando is admin of the team team.admin_role.members.add(rando) + inventory.read_role.members.add(rando) # team has read_role for the inventory team.member_role.children.add(inventory.read_role) - - role_pk = inventory.admin_role.pk - mock_access = mocker.MagicMock(spec=RoleAccess) - with mocker.patch('awx.main.access.RoleAccess', return_value=mock_access): - response = post(url=reverse('api:role_teams_list', args=(role_pk,)), - data={'id': team.pk}, user=rando) - mock_access.can_attach.assert_called_once_with( - inventory.admin_role, team, 'members', {"id": role_pk}, - skip_sub_obj_read_check=False) - assert team not in inventory.admin_role + + access = TeamAccess(rando) + data = {'id': inventory.admin_role.pk} + assert not access.can_attach(team, inventory.admin_role, 'member_role.children', data, False) @pytest.mark.django_db -def test_inventory_read_role_user_can_access(rando, inventory): - inventory.read_role.members.add(rando) - access = RoleAccess(rando) - assert not rando.can_access( - User, 'attach', rando, inventory.admin_role, 'roles', - {'id': inventory.admin_role.pk}, False) - -@pytest.mark.django_db -def test_inventory_read_role_user_access(rando, inventory): +def test_user_access_attach(rando, inventory): inventory.read_role.members.add(rando) access = UserAccess(rando) data = {'id': inventory.admin_role.pk} assert not access.can_attach(rando, inventory.admin_role, 'roles', data, False) @pytest.mark.django_db -def test_inventory_read_role_access(rando, inventory): +def test_role_access_attach(rando, inventory): inventory.read_role.members.add(rando) access = RoleAccess(rando) assert not access.can_attach(inventory.admin_role, rando, 'members', None)