diff --git a/awx/api/views.py b/awx/api/views.py index 4cdf4363f3..ef1697c782 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3769,22 +3769,27 @@ class RoleTeamsList(ListAPIView): return Team.objects.filter(member_role__children=role) def post(self, request, pk, *args, **kwargs): - # Forbid implicit role creation here + # Forbid implicit team creation here sub_id = request.data.get('id', None) if not sub_id: - data = dict(msg="Role 'id' field is missing.") + data = dict(msg="Team '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) + 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 a12c4b1426..16ca67a79a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -270,6 +270,19 @@ class UserAccess(BaseAccess): return True return 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', *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): ''' @@ -650,6 +663,24 @@ 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 an assignment + of a resource role to the team.""" + if isinstance(sub_obj, Role) and isinstance(sub_obj.content_object, ResourceMixin): + 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 isinstance(sub_obj, Role) and isinstance(sub_obj.content_object, ResourceMixin): + 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..5399356a21 --- /dev/null +++ b/awx/main/tests/functional/api/test_create_attach_views.py @@ -0,0 +1,45 @@ +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) + +@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) + +@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) diff --git a/awx/main/tests/functional/test_rbac_role.py b/awx/main/tests/functional/test_rbac_role.py new file mode 100644 index 0000000000..613051e395 --- /dev/null +++ b/awx/main/tests/functional/test_rbac_role.py @@ -0,0 +1,32 @@ +import pytest + +from awx.main.access import ( + RoleAccess, + UserAccess, + TeamAccess) + + +@pytest.mark.django_db +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) + + 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_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_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)