diff --git a/TODO.md b/TODO.md index 71ac687d58..596785d270 100644 --- a/TODO.md +++ b/TODO.md @@ -78,3 +78,9 @@ QUESTIONS * if creating a project, do we want to have an appliance style path for them, like /storage/projects/GUID ??? may want to keep somewhere else ? +MISC +---- + +when associating inventory objects, objects should share common inventory records (user_can_attach method should check) + + diff --git a/lib/main/base_views.py b/lib/main/base_views.py index 7f9ef92142..ef9b276225 100644 --- a/lib/main/base_views.py +++ b/lib/main/base_views.py @@ -123,6 +123,11 @@ class BaseSubList(BaseList): # now make sure we could have already attached the two together. If we could not have, raise an exception # such that the transaction does not commit. + + if main == obj: + # no attaching to yourself + raise PermissionDenied() + if self.__class__.parent_model != User: if not self.__class__.parent_model.can_user_attach(request.user, main, obj, self.__class__.relationship): raise PermissionDenied() diff --git a/lib/main/tests/inventory.py b/lib/main/tests/inventory.py index 416acb1c9b..54a7d34c06 100644 --- a/lib/main/tests/inventory.py +++ b/lib/main/tests/inventory.py @@ -212,6 +212,7 @@ class InventoryTest(BaseTest): got = self.get(url5, expect=200, auth=self.get_other_credentials()) self.assertEquals(got['count'], 3) + ################################################## # GROUPS->inventories POST via subcollection @@ -310,6 +311,38 @@ class InventoryTest(BaseTest): put = self.put(vdata1_url, data=vars_c, expect=200, auth=self.get_normal_credentials()) self.assertEquals(put, vars_c) + + #################################################### + # ADDING HOSTS TO GROUPS + + groups = Group.objects.all() + hosts = Host.objects.all() + groups[0].hosts.add(Host.objects.get(pk=1)) + groups[0].hosts.add(Host.objects.get(pk=3)) + groups[0].save() + + # access + url1 = '/api/v1/groups/1/hosts/' + data = self.get(url1, expect=200, auth=self.get_normal_credentials()) + self.assertEquals(data['count'], 2) + self.assertEquals(data['results'][0]['id'], 1) + self.assertEquals(data['results'][1]['id'], 3) + + # addition + got = self.get('/api/v1/hosts/2/', expect=200, auth=self.get_normal_credentials()) + self.assertEquals(got['id'], 2) + posted = self.post('/api/v1/groups/1/hosts/', data=got, expect=204, auth=self.get_normal_credentials()) + data = self.get(url1, expect=200, auth=self.get_normal_credentials()) + self.assertEquals(data['count'], 3) + self.assertEquals(data['results'][1]['id'], 2) + + # removal + got['disassociate'] = 1 + posted = self.post('/api/v1/groups/1/hosts/', data=got, expect=204, auth=self.get_normal_credentials()) + data = self.get(url1, expect=200, auth=self.get_normal_credentials()) + self.assertEquals(data['count'], 2) + self.assertEquals(data['results'][1]['id'], 3) + #################################################### # SUBGROUPS @@ -329,6 +362,12 @@ class InventoryTest(BaseTest): permission_type = PERM_INVENTORY_WRITE ) + # data used for testing listing all hosts that are transitive members of a group + g2 = Group.objects.get(pk=2) + nh = Host.objects.create(name='newhost.example.com', inventory=inv, created_by=User.objects.get(pk=1)) + g2.hosts.add(nh) + g2.save() + # a super user can set subgroups subgroups_url = '/api/v1/groups/1/children/' child_url = '/api/v1/groups/2/' @@ -355,6 +394,15 @@ class InventoryTest(BaseTest): self.post(subgroups_url3, data=got, expect=204, auth=self.get_other_credentials()) checked = self.get(subgroups_url3, expect=200, auth=self.get_normal_credentials()) self.assertEqual(checked['count'], 1) + + # slight detour + # can see all hosts under a group, even if it has subgroups + # this URL is NOT postable + all_hosts = '/api/v1/groups/1/all_hosts/' + self.assertEqual(Group.objects.get(pk=1).hosts.count(), 2) + data = self.get(all_hosts, expect=200, auth=self.get_normal_credentials()) + self.post(all_hosts, data=dict(id=123456, msg='spam'), expect=405, auth=self.get_normal_credentials()) + self.assertEquals(data['count'], 3) # now post it back to remove it, by adding the disassociate bit result = checked['results'][0] diff --git a/lib/main/views.py b/lib/main/views.py index 7837b9d552..44cc3be4c9 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -578,6 +578,83 @@ class GroupsChildrenList(BaseSubList): ).distinct() return admin_of | has_user_perms | has_team_perms +class GroupsHostsList(BaseSubList): + ''' the list of hosts directly below a group ''' + + model = Host + serializer_class = HostSerializer + permission_classes = (CustomRbac,) + parent_model = Group + relationship = 'hosts' + postable = True + inject_primary_key_on_post_as = 'group' + + def _get_queryset(self): + + parent = Group.objects.get(pk=self.kwargs['pk']) + + # FIXME: verify read permissions on this object are still required at a higher level + + base = parent.hosts + if self.request.user.is_superuser: + return base.all() + admin_of = base.filter(inventory__organization__admins__in = [ self.request.user ]).distinct() + has_user_perms = base.filter( + inventory__permissions__user__in = [ self.request.user ], + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + inventory__permissions__team__in = self.request.user.teams.all(), + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + + +class GroupsAllHostsList(BaseSubList): + ''' the list of all hosts below a group, even including subgroups ''' + + model = Host + serializer_class = HostSerializer + permission_classes = (CustomRbac,) + parent_model = Group + relationship = 'hosts' + + def _child_hosts(self, parent): + # TODO: should probably be a method on the model + result = parent.hosts.distinct() + if parent.children.count() == 0: + return result + else: + for child in parent.children.all(): + if child == parent: + # shouldn't happen, but be prepared in case DB is weird + continue + result = result | self._child_hosts(child) + return result + + def _get_queryset(self): + + parent = Group.objects.get(pk=self.kwargs['pk']) + + # FIXME: verify read permissions on this object are still required at a higher level + + base = self._child_hosts(parent) + + if self.request.user.is_superuser: + return base.all() + + admin_of = base.filter(inventory__organization__admins__in = [ self.request.user ]).distinct() + has_user_perms = base.filter( + inventory__permissions__user__in = [ self.request.user ], + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + inventory__permissions__team__in = self.request.user.teams.all(), + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + + class GroupsDetail(BaseDetail): model = Group diff --git a/lib/urls.py b/lib/urls.py index 0ae9fca028..d9057020d9 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -66,6 +66,8 @@ views_GroupsList = views.GroupsList.as_view() views_GroupsDetail = views.GroupsDetail.as_view() views_GroupsVariableDetail = views.GroupsVariableDetail.as_view() views_GroupsChildrenList = views.GroupsChildrenList.as_view() +views_GroupsAllHostsList = views.GroupsAllHostsList.as_view() +views_GroupsHostsList = views.GroupsHostsList.as_view() # host service views_HostsList = views.HostsList.as_view() @@ -147,6 +149,8 @@ urlpatterns = patterns('', url(r'^api/v1/groups/$', views_GroupsList), url(r'^api/v1/groups/(?P[0-9]+)/$', views_GroupsDetail), url(r'^api/v1/groups/(?P[0-9]+)/children/$', views_GroupsChildrenList), + url(r'^api/v1/groups/(?P[0-9]+)/hosts/$', views_GroupsHostsList), + url(r'^api/v1/groups/(?P[0-9]+)/all_hosts/$', views_GroupsAllHostsList), # variable data url(r'^api/v1/hosts/(?P[0-9]+)/variable_data/$', views_HostsVariableDetail),