From 38f03ea32f31de3ddfd7222d0d173cc163460dab Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 17 Jul 2016 08:04:06 -0400 Subject: [PATCH 01/15] Allow auditors to see same /api/v1/config information as admins --- awx/api/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 11ae8f6766..f71d799ab4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -201,7 +201,7 @@ class ApiV1ConfigView(APIView): '''Return various sitewide configuration settings.''' license_reader = TaskSerializer() - license_data = license_reader.from_database(show_key=request.user.is_superuser) + license_data = license_reader.from_database(show_key=request.user.is_superuser or request.user.is_system_auditor) if license_data and 'features' in license_data and 'activity_streams' in license_data['features']: license_data['features']['activity_streams'] &= tower_settings.ACTIVITY_STREAM_ENABLED @@ -225,7 +225,10 @@ class ApiV1ConfigView(APIView): user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) data['user_ldap_fields'] = user_ldap_fields - if request.user.is_superuser or Organization.accessible_objects(request.user, 'admin_role').exists(): + if request.user.is_superuser \ + or request.user.is_system_auditor \ + or Organization.accessible_objects(request.user, 'admin_role').exists() \ + or Organization.accessible_objects(request.user, 'auditor_role').exists(): data.update(dict( project_base_dir = settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), From f154633277c70a9ad830e98ff51dd2f331752b0b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 18 Jul 2016 10:55:53 -0400 Subject: [PATCH 02/15] pass context to unified job template subclasses --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 679d23aeee..130cc8112c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -527,7 +527,7 @@ class UnifiedJobTemplateSerializer(BaseSerializer): elif isinstance(obj, JobTemplate): serializer_class = JobTemplateSerializer if serializer_class: - serializer = serializer_class(instance=obj) + serializer = serializer_class(instance=obj, context=self.context) return serializer.to_representation(obj) else: return super(UnifiedJobTemplateSerializer, self).to_representation(obj) From 29e35357e921d99b4c3d58346dbfd6d303223d94 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 18 Jul 2016 11:10:55 -0400 Subject: [PATCH 03/15] pass context into Job and JobList serializer classes --- awx/api/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 130cc8112c..5c7e51d601 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -590,7 +590,7 @@ class UnifiedJobSerializer(BaseSerializer): elif isinstance(obj, SystemJob): serializer_class = SystemJobSerializer if serializer_class: - serializer = serializer_class(instance=obj) + serializer = serializer_class(instance=obj, context=self.context) ret = serializer.to_representation(obj) else: ret = super(UnifiedJobSerializer, self).to_representation(obj) @@ -637,7 +637,7 @@ class UnifiedJobListSerializer(UnifiedJobSerializer): elif isinstance(obj, SystemJob): serializer_class = SystemJobListSerializer if serializer_class: - serializer = serializer_class(instance=obj) + serializer = serializer_class(instance=obj, context=self.context) ret = serializer.to_representation(obj) else: ret = super(UnifiedJobListSerializer, self).to_representation(obj) From fc9695208416924e99ed08ccc0006764696be042 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 20 Jul 2016 10:21:38 -0400 Subject: [PATCH 04/15] orphan project protection in job delete access --- awx/main/access.py | 3 ++- awx/main/tests/functional/test_rbac_job.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/awx/main/access.py b/awx/main/access.py index d3f8e50990..0d09c57f5e 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1081,7 +1081,8 @@ class JobAccess(BaseAccess): def can_delete(self, obj): if obj.inventory is not None and self.user in obj.inventory.organization.admin_role: return True - if obj.project is not None and self.user in obj.project.organization.admin_role: + if (obj.project is not None and obj.project.organization is not None and + self.user in obj.project.organization.admin_role): return True return False diff --git a/awx/main/tests/functional/test_rbac_job.py b/awx/main/tests/functional/test_rbac_job.py index f1688b7046..febade67eb 100644 --- a/awx/main/tests/functional/test_rbac_job.py +++ b/awx/main/tests/functional/test_rbac_job.py @@ -92,6 +92,12 @@ def test_null_related_delete_denied(normal_job, rando): access = JobAccess(rando) assert not access.can_delete(normal_job) +@pytest.mark.django_db +def test_delete_job_with_orphan_proj(normal_job, rando): + normal_job.project.organization = None + access = JobAccess(rando) + assert not access.can_delete(normal_job) + @pytest.mark.django_db def test_inventory_org_admin_delete_allowed(normal_job, org_admin): normal_job.project = None # do this so we test job->inventory->org->admin connection From 7e49a428e685128662be54e830486416dc7adda6 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 21 Jul 2016 09:05:32 -0400 Subject: [PATCH 05/15] Allow instant cancel for new jobs --- awx/main/models/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 36b2dfe75e..f7106eb7ab 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -873,7 +873,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if not self.cancel_flag: self.cancel_flag = True cancel_fields = ['cancel_flag'] - if self.status in ('pending', 'waiting'): + if self.status in ('pending', 'waiting', 'new'): self.status = 'canceled' cancel_fields.append('status') self.save(update_fields=cancel_fields) From 6fee46fb666c89ec037a346fb7854f341ed59882 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 22 Jul 2016 12:47:36 -0400 Subject: [PATCH 06/15] Reorganize activity stream around org admin/auditors --- awx/main/access.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 0d09c57f5e..6bd920c7fe 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1562,21 +1562,22 @@ class ActivityStreamAccess(BaseAccess): inventory_set = Inventory.accessible_objects(self.user, 'read_role') credential_set = Credential.accessible_objects(self.user, 'read_role') - organization_set = Organization.accessible_objects(self.user, 'read_role') - admin_of_orgs = Organization.accessible_objects(self.user, 'admin_role') - group_set = Group.objects.filter(inventory__in=inventory_set) + auditing_orgs = ( + Organization.accessible_objects(self.user, 'admin_role') | + Organization.accessible_objects(self.user, 'auditor_role') + ).distinct().values_list('id', flat=True) project_set = Project.accessible_objects(self.user, 'read_role') jt_set = JobTemplate.accessible_objects(self.user, 'read_role') team_set = Team.accessible_objects(self.user, 'read_role') return qs.filter( Q(ad_hoc_command__inventory__in=inventory_set) | - Q(user__in=organization_set.values('member_role__members')) | + Q(user__in=auditing_orgs.values('member_role__members')) | Q(user=self.user) | - Q(organization__in=organization_set) | + Q(organization__in=auditing_orgs) | Q(inventory__in=inventory_set) | Q(host__inventory__in=inventory_set) | - Q(group__in=group_set) | + Q(group__inventory__in=inventory_set) | Q(inventory_source__inventory__in=inventory_set) | Q(inventory_update__inventory_source__inventory__in=inventory_set) | Q(credential__in=credential_set) | @@ -1585,10 +1586,10 @@ class ActivityStreamAccess(BaseAccess): Q(project_update__project__in=project_set) | Q(job_template__in=jt_set) | Q(job__job_template__in=jt_set) | - Q(notification_template__organization__in=admin_of_orgs) | - Q(notification__notification_template__organization__in=admin_of_orgs) | - Q(label__organization__in=organization_set) | - Q(role__in=Role.visible_roles(self.user)) + Q(notification_template__organization__in=auditing_orgs) | + Q(notification__notification_template__organization__in=auditing_orgs) | + Q(label__organization__in=auditing_orgs) | + Q(role__in=Role.visible_roles(self.user) if auditing_orgs else []) ).distinct() def can_add(self, data): From e27ce600c5ef21475b409c11b1eb3b01a7820d7c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 27 Jul 2016 10:28:53 -0400 Subject: [PATCH 07/15] add system job templates to the unified JT list --- awx/api/serializers.py | 2 ++ awx/main/access.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 5c7e51d601..05fbf793c9 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -526,6 +526,8 @@ class UnifiedJobTemplateSerializer(BaseSerializer): serializer_class = InventorySourceSerializer elif isinstance(obj, JobTemplate): serializer_class = JobTemplateSerializer + elif isinstance(obj, SystemJobTemplate): + serializer_class = SystemJobTemplateSerializer if serializer_class: serializer = serializer_class(instance=obj, context=self.context) return serializer.to_representation(obj) diff --git a/awx/main/access.py b/awx/main/access.py index 0d09c57f5e..7053036be3 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1292,9 +1292,11 @@ class UnifiedJobTemplateAccess(BaseAccess): project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES]) inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES) job_template_qs = self.user.get_queryset(JobTemplate) + system_job_template_qs = self.user.get_queryset(SystemJobTemplate) qs = qs.filter(Q(Project___in=project_qs) | Q(InventorySource___in=inventory_source_qs) | - Q(JobTemplate___in=job_template_qs)) + Q(JobTemplate___in=job_template_qs) | + Q(systemjobtemplate__in=system_job_template_qs)) qs = qs.select_related( 'created_by', 'modified_by', From 1fb173b2e5f770a0fab64bf71c16ca0cebc9ac46 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 28 Jul 2016 11:15:08 -0400 Subject: [PATCH 08/15] RoleTeam and TeamRole sublist NotFound exception handling and test update --- awx/api/views.py | 4 ++-- awx/main/tests/unit/api/test_views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index f71d799ab4..1edeb4eb39 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -879,7 +879,7 @@ class TeamRolesList(SubListCreateAttachDetachAPIView): data = dict(msg="Role 'id' field is missing.") return Response(data, status=status.HTTP_400_BAD_REQUEST) - role = Role.objects.get(pk=sub_id) + role = get_object_or_400(Role, pk=sub_id) content_type = ContentType.objects.get_for_model(Organization) if role.content_type == content_type: data = dict(msg="You cannot assign an Organization role as a child role for a Team.") @@ -3678,7 +3678,7 @@ class RoleTeamsList(SubListAPIView): data = dict(msg="You cannot assign an Organization role as a child role for a Team.") return Response(data, status=status.HTTP_400_BAD_REQUEST) - team = Team.objects.get(pk=sub_id) + team = get_object_or_400(Team, pk=sub_id) action = 'attach' if request.data.get('disassociate', None): action = 'unattach' diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index a03ef7adae..c667be6450 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -74,13 +74,13 @@ class TestJobTemplateLabelList: @pytest.mark.parametrize("url", ["/team/1/roles", "/role/1/teams"]) def test_team_roles_list_post_org_roles(url): - with mock.patch('awx.api.views.Role.objects.get') as role_get, \ + with mock.patch('awx.api.views.get_object_or_400') as mock_get_obj, \ 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 + mock_get_obj.return_value = role_mock ct_get.return_value = content_type_mock factory = APIRequestFactory() From 06bb8871d787260d4153aa495ccd795fd80c997b Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 28 Jul 2016 19:26:30 -0400 Subject: [PATCH 09/15] do not allow membership changes to User.admin_role --- awx/api/views.py | 7 ++++++- awx/main/tests/functional/api/test_user.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 1edeb4eb39..0fd632aa5a 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1208,7 +1208,12 @@ class UserRolesList(SubListCreateAttachDetachAPIView): return Response(data, status=status.HTTP_400_BAD_REQUEST) if sub_id == self.request.user.admin_role.pk: - raise PermissionDenied('You may not remove your own admin_role.') + raise PermissionDenied('You may not perform any action with your own admin_role.') + + role = get_object_or_404(Role, pk=sub_id) + user_content_type = ContentType.objects.get_for_model(User) + if role.content_type == user_content_type: + raise PermissionDenied('You may not change the membership of a users admin_role') return super(UserRolesList, self).post(request, *args, **kwargs) diff --git a/awx/main/tests/functional/api/test_user.py b/awx/main/tests/functional/api/test_user.py index d739d417c0..4ebd46f225 100644 --- a/awx/main/tests/functional/api/test_user.py +++ b/awx/main/tests/functional/api/test_user.py @@ -66,3 +66,13 @@ def test_create_delete_create_user(post, delete, admin): }, admin) print(response.data) assert response.status_code == 201 + +@pytest.mark.django_db +def test_add_user_admin_role_member(post, user): + admin = user('admin', is_superuser=True) + normal = user('normal') + + url = reverse('api:user_roles_list', args=(admin.pk,)) + response = post(url, {'id':normal.admin_role.pk}, admin) + assert response.status_code == 403 + assert 'not change membership' in response.rendered_content From a431ac785407fad120b4adcbde96060992dc96a0 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 28 Jul 2016 20:49:31 -0400 Subject: [PATCH 10/15] fix test --- awx/main/tests/functional/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/tests/functional/api/test_user.py b/awx/main/tests/functional/api/test_user.py index 4ebd46f225..027acc0703 100644 --- a/awx/main/tests/functional/api/test_user.py +++ b/awx/main/tests/functional/api/test_user.py @@ -75,4 +75,4 @@ def test_add_user_admin_role_member(post, user): url = reverse('api:user_roles_list', args=(admin.pk,)) response = post(url, {'id':normal.admin_role.pk}, admin) assert response.status_code == 403 - assert 'not change membership' in response.rendered_content + assert 'not change the membership' in response.rendered_content From 52865eea6a0d745c0f38033daa5145ed83f7a265 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 29 Jul 2016 09:42:04 -0400 Subject: [PATCH 11/15] restrict User.admin_role membership changes through RoleUsersList --- awx/api/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/awx/api/views.py b/awx/api/views.py index 0fd632aa5a..65fbc03e64 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3653,6 +3653,15 @@ class RoleUsersList(SubListCreateAttachDetachAPIView): if not sub_id: data = dict(msg="User 'id' field is missing.") return Response(data, status=status.HTTP_400_BAD_REQUEST) + + role = self.get_parent_object() + if role == self.request.user.admin_role: + raise PermissionDenied('You may not perform any action with your own admin_role.') + + user_content_type = ContentType.objects.get_for_model(User) + if role.content_type == user_content_type: + raise PermissionDenied('You may not change the membership of a users admin_role') + return super(RoleUsersList, self).post(request, *args, **kwargs) From b127e74ae414bcda79f3b501ce5d3af13c6be8c0 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 29 Jul 2016 10:37:49 -0400 Subject: [PATCH 12/15] refactor to unit tests --- awx/api/views.py | 2 +- awx/main/tests/functional/api/test_user.py | 10 --- awx/main/tests/unit/api/test_roles.py | 77 ++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 awx/main/tests/unit/api/test_roles.py diff --git a/awx/api/views.py b/awx/api/views.py index 65fbc03e64..23a4c8cc64 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1210,7 +1210,7 @@ class UserRolesList(SubListCreateAttachDetachAPIView): if sub_id == self.request.user.admin_role.pk: raise PermissionDenied('You may not perform any action with your own admin_role.') - role = get_object_or_404(Role, pk=sub_id) + role = Role.objects.get(pk=sub_id) user_content_type = ContentType.objects.get_for_model(User) if role.content_type == user_content_type: raise PermissionDenied('You may not change the membership of a users admin_role') diff --git a/awx/main/tests/functional/api/test_user.py b/awx/main/tests/functional/api/test_user.py index 027acc0703..d739d417c0 100644 --- a/awx/main/tests/functional/api/test_user.py +++ b/awx/main/tests/functional/api/test_user.py @@ -66,13 +66,3 @@ def test_create_delete_create_user(post, delete, admin): }, admin) print(response.data) assert response.status_code == 201 - -@pytest.mark.django_db -def test_add_user_admin_role_member(post, user): - admin = user('admin', is_superuser=True) - normal = user('normal') - - url = reverse('api:user_roles_list', args=(admin.pk,)) - response = post(url, {'id':normal.admin_role.pk}, admin) - assert response.status_code == 403 - assert 'not change the membership' in response.rendered_content diff --git a/awx/main/tests/unit/api/test_roles.py b/awx/main/tests/unit/api/test_roles.py new file mode 100644 index 0000000000..e15e691af9 --- /dev/null +++ b/awx/main/tests/unit/api/test_roles.py @@ -0,0 +1,77 @@ +import mock +from mock import PropertyMock + +import pytest + +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 ( + RoleUsersList, + UserRolesList, +) + +from awx.main.models import ( + User, + Role, +) + +@pytest.mark.parametrize("pk, err", [ + (111, "not change the membership"), + (1, "may not perform"), +]) +def test_user_roles_list_user_admin_role(pk, err): + with mock.patch('awx.api.views.Role.objects.get') as role_get, \ + mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get: + + role_mock = mock.MagicMock(spec=Role, id=1, pk=1) + 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 + + with mock.patch('awx.api.views.User.admin_role', new_callable=PropertyMock, return_value=role_mock): + factory = APIRequestFactory() + view = UserRolesList.as_view() + + user = User(username="root", is_superuser=True) + + request = factory.post("/user/1/roles", {'id':pk}, format="json") + force_authenticate(request, user) + + response = view(request) + response.render() + + assert response.status_code == 403 + assert err in response.content + +@pytest.mark.parametrize("admin_role, err", [ + (True, "may not perform"), + (False, "not change the membership"), +]) +def test_role_users_list_other_user_admin_role(admin_role, err): + with mock.patch('awx.api.views.RoleUsersList.get_parent_object') as role_get, \ + mock.patch('awx.api.views.ContentType.objects.get_for_model') as ct_get: + + role_mock = mock.MagicMock(spec=Role, id=1) + 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 + + user_admin_role = role_mock if admin_role else None + with mock.patch('awx.api.views.User.admin_role', new_callable=PropertyMock, return_value=user_admin_role): + factory = APIRequestFactory() + view = RoleUsersList.as_view() + + user = User(username="root", is_superuser=True, pk=1, id=1) + request = factory.post("/role/1/users", {'id':1}, format="json") + force_authenticate(request, user) + + response = view(request) + response.render() + + assert response.status_code == 403 + assert err in response.content From 9baa9594c7284273abe40341b9ab89207927fb66 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 29 Jul 2016 11:19:43 -0400 Subject: [PATCH 13/15] use get_object_or_400 to fetch Role --- awx/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 23a4c8cc64..32fd3c5b45 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1210,7 +1210,7 @@ class UserRolesList(SubListCreateAttachDetachAPIView): if sub_id == self.request.user.admin_role.pk: raise PermissionDenied('You may not perform any action with your own admin_role.') - role = Role.objects.get(pk=sub_id) + role = get_object_or_400(Role, pk=sub_id) user_content_type = ContentType.objects.get_for_model(User) if role.content_type == user_content_type: raise PermissionDenied('You may not change the membership of a users admin_role') From b862a13d92ad68e0d1a9609fe77fc208b94f9351 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 29 Jul 2016 11:20:05 -0400 Subject: [PATCH 14/15] update unit tetsts --- awx/main/tests/unit/api/test_roles.py | 25 +++++++++++++++++- awx/main/tests/unit/api/test_views.py | 37 +-------------------------- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/awx/main/tests/unit/api/test_roles.py b/awx/main/tests/unit/api/test_roles.py index e15e691af9..7a5112ceae 100644 --- a/awx/main/tests/unit/api/test_roles.py +++ b/awx/main/tests/unit/api/test_roles.py @@ -11,6 +11,7 @@ from django.contrib.contenttypes.models import ContentType from awx.api.views import ( RoleUsersList, UserRolesList, + TeamRolesList, ) from awx.main.models import ( @@ -23,7 +24,7 @@ from awx.main.models import ( (1, "may not perform"), ]) def test_user_roles_list_user_admin_role(pk, err): - with mock.patch('awx.api.views.Role.objects.get') as role_get, \ + 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, id=1, pk=1) @@ -75,3 +76,25 @@ def test_role_users_list_other_user_admin_role(admin_role, err): assert response.status_code == 403 assert err in response.content + +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 diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index c667be6450..6a97831f02 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -1,22 +1,11 @@ import mock import pytest -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 ( ApiV1RootView, - TeamRolesList, JobTemplateLabelList, ) -from awx.main.models import ( - User, - Role, -) - @pytest.fixture def mock_response_new(mocker): m = mocker.patch('awx.api.views.Response.__new__') @@ -68,30 +57,6 @@ class TestJobTemplateLabelList: with mock.patch('awx.api.generics.DeleteLastUnattachLabelMixin.unattach') as mixin_unattach: view = JobTemplateLabelList() mock_request = mock.MagicMock() - + super(JobTemplateLabelList, view).unattach(mock_request, None, None) assert mixin_unattach.called_with(mock_request, None, None) - -@pytest.mark.parametrize("url", ["/team/1/roles", "/role/1/teams"]) -def test_team_roles_list_post_org_roles(url): - with mock.patch('awx.api.views.get_object_or_400') as mock_get_obj, \ - 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 - mock_get_obj.return_value = role_mock - ct_get.return_value = content_type_mock - - factory = APIRequestFactory() - view = TeamRolesList.as_view() - - request = factory.post(url, {'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 - From 5dbbaf4105b9fe52526ba590d086710930fa7808 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 29 Jul 2016 12:02:40 -0400 Subject: [PATCH 15/15] add code to HostAccess can_add so the browsable API will work --- awx/main/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index fe1dfb7bbc..7d4dbd1c8d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -427,8 +427,8 @@ class HostAccess(BaseAccess): return obj and self.user in obj.inventory.read_role def can_add(self, data): - if not data or 'inventory' not in data: - return False + if not data: # So the browseable API will work + return Inventory.accessible_objects(self.user, 'admin_role').exists() # Checks for admin or change permission on inventory. inventory_pk = get_pk_from_dict(data, 'inventory')