From a6c767907e4fc97ef76983fca9d878031774d176 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Fri, 19 Jul 2013 00:33:56 -0400 Subject: [PATCH] Renamed some API files/classes to mimic REST framework names, moved queryset filtering for permissions alongside other permissions/access checks, cleaned up base views to handle get_queryset based on class attributes, cleaned up post to sublist to create/attach/unattach. --- awx/main/access.py | 162 ++++- awx/main/authentication.py | 3 + awx/main/base_views.py | 314 ++++----- awx/main/compat.py | 3 + awx/main/{custom_filters.py => filters.py} | 4 +- awx/main/models/__init__.py | 19 + awx/main/{rbac.py => permissions.py} | 70 +- awx/main/tasks.py | 21 +- awx/main/tests/commands.py | 4 +- awx/main/views.py | 739 ++++----------------- awx/settings/defaults.py | 7 +- 11 files changed, 494 insertions(+), 852 deletions(-) rename awx/main/{custom_filters.py => filters.py} (92%) rename awx/main/{rbac.py => permissions.py} (77%) diff --git a/awx/main/access.py b/awx/main/access.py index c58c118f47..c77bc2ebb5 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1,15 +1,18 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. +# Python import sys import logging +# Django from django.db.models import Q from django.contrib.auth.models import User +# Django REST Framework from rest_framework.exceptions import PermissionDenied -from awx import MODE +# AWX from awx.main.models import * from awx.main.licenses import LicenseReader @@ -63,6 +66,12 @@ def check_user_access(user, model_class, action, *args, **kwargs): return False class BaseAccess(object): + ''' + Base class for checking user access to a given model. Subclasses should + define the model attribute, override the get_queryset method to return only + the instances the user should be able to view, and override/define can_* + methods to verify a user's permission to perform a particular action. + ''' model = None @@ -102,8 +111,7 @@ class BaseAccess(object): return self.can_change(obj, None) else: return bool(self.can_change(obj, None) and - check_user_access(self.user, type(sub_obj), 'read', - sub_obj)) + self.user.can_access(type(sub_obj), 'read', sub_obj)) def can_unattach(self, obj, sub_obj, relationship): return self.can_change(obj, None) @@ -120,7 +128,7 @@ class UserAccess(BaseAccess): return self.model.objects.filter(is_active=True).filter( Q(pk=self.user.pk) | Q(organizations__in=self.user.admin_of_organizations.all()) | - Q(teams__in=self.request.user.teams.all()) + Q(teams__in=self.user.teams.all()) ).distinct() def can_read(self, obj): @@ -158,6 +166,14 @@ class OrganizationAccess(BaseAccess): model = Organization + def get_queryset(self): + # I can see organizations when I am a superuser, or I am an admin or + # user in that organization. + qs = self.model.objects.distinct() + if self.user.is_superuser: + return qs + return qs.filter(Q(admins__in=[self.user]) | Q(users__in=[self.user])) + def can_read(self, obj): return bool(self.can_change(obj, None) or self.user in obj.users.all()) @@ -174,6 +190,23 @@ class InventoryAccess(BaseAccess): model = Inventory + def get_queryset(self): + # I can see inventory when I'm a superuser, an org admin of the + # inventory, or I have permissions on it. + base = Inventory.objects.distinct() + if self.user.is_superuser: + return base.all() + admin_of = base.filter(organization__admins__in = [ self.user ]).distinct() + has_user_perms = base.filter( + permissions__user__in = [ self.user ], + permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + permissions__team__in = self.user.teams.all(), + permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + def _has_permission_types(self, obj, allowed): if self.user.is_superuser: return True @@ -241,7 +274,7 @@ class InventoryAccess(BaseAccess): ''' whether you can add sub_obj to obj using the relationship type in a subobject view ''' #if not sub_obj.can_user_read(user, sub_obj): if sub_obj and not skip_sub_obj_read_check: - if not check_user_access(self.user, type(sub_obj), 'read', sub_obj): + if not self.user.can_access(type(sub_obj), 'read', sub_obj): return False return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) @@ -252,8 +285,29 @@ class HostAccess(BaseAccess): model = Host + def get_queryset(self): + ''' + I can see hosts when: + I'm a superuser, + or an organization admin of an inventory they are in + or when I have allowing read permissions via a user or team on an inventory they are in + ''' + base = self.model.objects + if self.user.is_superuser: + return base.all() + admin_of = base.filter(inventory__organization__admins__in = [ self.user ]).distinct() + has_user_perms = base.filter( + inventory__permissions__user__in = [ self.user ], + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + inventory__permissions__team__in = self.user.teams.all(), + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + def can_read(self, obj): - return check_user_access(self.user, Inventory, 'read', obj.inventory) + return self.user.can_access(Inventory, 'read', obj.inventory) def can_add(self, data): @@ -264,7 +318,7 @@ class HostAccess(BaseAccess): inventory = Inventory.objects.get(pk=data['inventory']) # Checks for admin or change permission on inventory. - permissions_ok = check_user_access(self.user, Inventory, 'change', inventory, None) + permissions_ok = self.user.can_access(Inventory, 'change', inventory, None) if not permissions_ok: return False @@ -286,31 +340,82 @@ class HostAccess(BaseAccess): def can_change(self, obj, data): # Checks for admin or change permission on inventory, controls whether # the user can edit variable data. - return check_user_access(self.user, Inventory, 'change', obj.inventory, None) + return self.user.can_access(Inventory, 'change', obj.inventory, None) class GroupAccess(BaseAccess): model = Group + def get_queryset(self): + ''' + I can see groups when: + I'm a superuser, + or an organization admin of an inventory they are in + or when I have allowing read permissions via a user or team on an inventory they are in + ''' + base = Group.objects + if self.user.is_superuser: + return base.distinct() + admin_of = base.filter(inventory__organization__admins__in = [ self.user ]).distinct() + has_user_perms = base.filter( + inventory__permissions__user__in = [ self.user ], + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + has_team_perms = base.filter( + inventory__permissions__team__in = self.user.teams.all(), + inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, + ).distinct() + return admin_of | has_user_perms | has_team_perms + def can_read(self, obj): - return check_user_access(self.user, Inventory, 'read', obj.inventory) + return self.user.can_access(Inventory, 'read', obj.inventory) def can_add(self, data): if not 'inventory' in data: return False inventory = Inventory.objects.get(pk=data['inventory']) # Checks for admin or change permission on inventory. - return check_user_access(self.user, Inventory, 'change', inventory, None) + return self.user.can_access(Inventory, 'change', inventory, None) def can_change(self, obj, data): # Checks for admin or change permission on inventory, controls whether # the user can attach subgroups or edit variable data. - return check_user_access(self.user, Inventory, 'change', obj.inventory, None) + return self.user.can_access(Inventory, 'change', obj.inventory, None) + + def can_attach(self, obj, sub_obj, relationship, data, + skip_sub_obj_read_check=False): + if not self.can_change(obj, None): + return False + if sub_obj and not skip_sub_obj_read_check: + if not self.user.can_access(type(sub_obj), 'read', sub_obj): + return False + + # Prevent group from being assigned as its own (grand)child. + if type(obj) == type(sub_obj): + parent_pks = set(obj.all_parents.values_list('pk', flat=True)) + parent_pks.add(obj.pk) + child_pks = set(sub_obj.all_children.values_list('pk', flat=True)) + child_pks.add(sub_obj.pk) + if parent_pks & child_pks: + return False + + return True class CredentialAccess(BaseAccess): model = Credential + def get_queryset(self): + # I can see credentials when: + # - It's a user credential and it's my credential. + # - It's a user credential and I'm an admin of an organization + # - + # FIXME + qs = self.model.objects.distinct() + if self.user.is_superuser: + return qs + return qs.filter(Q(user=self.user)) + def can_read(self, obj): return self.can_change(obj, None) @@ -319,10 +424,10 @@ class CredentialAccess(BaseAccess): return True if 'user' in data: user_obj = User.objects.get(pk=data['user']) - return check_user_access(self.user, User, 'change', user_obj, None) + return self.user.can_access(User, 'change', user_obj, None) if 'team' in data: team_obj = Team.objects.get(pk=data['team']) - return check_user_access(self.user, Team, 'change', team_obj, None) + return self.user.can_access(Team, 'change', team_obj, None) def can_change(self, obj, data): if self.user.is_superuser: @@ -347,6 +452,9 @@ class TeamAccess(BaseAccess): model = Team + def get_queryset(self): + return self.model.objects.distinct() # FIXME + def can_add(self, data): if self.user.is_superuser: return True @@ -378,6 +486,22 @@ class ProjectAccess(BaseAccess): model = Project + def get_queryset(self): + # I can see projects when: + # - I am a superuser + # - I am an admin or user in that organization... + # FIXME + base = Project.objects.distinct() + if self.user.is_superuser: + return base.all() + my_teams = Team.objects.filter(users__in = [ self.user]) + my_orgs = Organization.objects.filter(admins__in = [ self.user ]) + return base.filter( + teams__in = my_teams + ).distinct() | base.filter( + organizations__in = my_orgs + ).distinct() + def can_read(self, obj): if self.can_change(obj, None): return True @@ -403,6 +527,9 @@ class PermissionAccess(BaseAccess): model = Permission + def get_queryset(self): + return self.model.objects.distinct() # FIXME + def can_read(self, obj): # a permission can be seen by the assigned user or team # or anyone who can administrate that permission @@ -524,6 +651,9 @@ class JobAccess(BaseAccess): model = Job + def get_queryset(self): + return self.model.objects.distinct() # FIXME + def can_change(self, obj, data): return self.user.is_superuser and obj.status == 'new' @@ -537,10 +667,16 @@ class JobHostSummaryAccess(BaseAccess): model = JobHostSummary + def get_queryset(self): + return self.model.objects.distinct() # FIXME + class JobEventAccess(BaseAccess): model = JobEvent + def get_queryset(self): + return self.model.objects.distinct() # FIXME + register_access(User, UserAccess) register_access(Organization, OrganizationAccess) register_access(Inventory, InventoryAccess) diff --git a/awx/main/authentication.py b/awx/main/authentication.py index 1c5af775aa..6e01a0832c 100644 --- a/awx/main/authentication.py +++ b/awx/main/authentication.py @@ -1,3 +1,6 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + # Django REST Framework from rest_framework import authentication from rest_framework import exceptions diff --git a/awx/main/base_views.py b/awx/main/base_views.py index ad8f6dc27d..93bee79bab 100644 --- a/awx/main/base_views.py +++ b/awx/main/base_views.py @@ -6,66 +6,90 @@ import json # Django from django.http import HttpResponse, Http404 -from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 from django.utils.timezone import now # Django REST Framework from rest_framework.exceptions import PermissionDenied -from rest_framework import mixins from rest_framework import generics -from rest_framework import permissions from rest_framework.response import Response from rest_framework import status # AWX from awx.main.models import * -from awx.main.serializers import * -from awx.main.rbac import * -from awx.main.access import * # FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS -class BaseList(generics.ListCreateAPIView): - - permission_classes = (CustomRbac,) +class ListAPIView(generics.ListAPIView): + # Base class for a read-only list view. # Subclasses should define: # model = ModelClass # serializer_class = SerializerClass - def post(self, request, *args, **kwargs): - # FIXME: Should inherit from generics.ListAPIView if not postable. - postable = getattr(self.__class__, 'postable', True) - if not postable: - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - return super(BaseList, self).post(request, *args, **kwargs) + def get_queryset(self): + return self.request.user.get_queryset(self.model) - # NOTE: Moved filtering from get_queryset into custom filter backend. +class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): + # Base class for a list view that allows creating new objects. + pass -class BaseSubList(BaseList): - - ''' used for subcollections with an overriden post ''' +class SubListAPIView(ListAPIView): + # Base class for a read-only sublist view. # Subclasses should define at least: # model = ModelClass # serializer_class = SerializerClass # parent_model = ModelClass # relationship = 'rel_name_from_parent_to_model' - # And optionally: - # postable = True/False + # And optionally (user must have given access permission on parent object + # to view sublist): + # parent_access = 'admin' + + def get_parent_object(self): + parent_filter = { + self.lookup_field: self.kwargs.get(self.lookup_field, None), + } + return get_object_or_404(self.parent_model, **parent_filter) + + def check_parent_access(self, parent=None): + parent = parent or self.get_parent_object() + parent_access = getattr(self, 'parent_access', 'admin') + if parent_access in ('read', 'delete'): + args = (self.parent_model, parent_access, parent) + else: + args = (self.parent_model, parent_access, parent, None) + if not self.request.user.can_access(*args): + raise PermissionDenied() + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model).distinct() + sublist_qs = getattr(parent, self.relationship).distinct() + return qs & sublist_qs + +class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView): + # Base class for a sublist view that allows for creating subobjects and + # attaching/detaching them from the parent. + + # In addition to SubListAPIView properties, subclasses may define: # inject_primary_key_on_post_as = 'field_on_model_referring_to_parent' # severable = True/False - def post(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs): + # If the object ID was not specified, it probably doesn't exist in the + # DB yet. We want to see if we can create it. The URL may choose to + # inject it's primary key into the object because we are posting to a + # subcollection. Use all the normal access control mechanisms. - # decide whether to return 201 with data (new object) or 204 (just associated) - created = False - ser = None + inject_primary_key = getattr(self, 'inject_primary_key_on_post_as', None) - postable = getattr(self.__class__, 'postable', False) - if not postable: - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + if inject_primary_key is None: + # view didn't specify a way to get the pk from the URL, so not even trying + return Response(status=status.HTTP_400_BAD_REQUEST, + data=dict(msg='object cannot be created')) # Make a copy of the data provided (since it's readonly) in order to # inject additional data. @@ -73,157 +97,100 @@ class BaseSubList(BaseList): data = request.DATA.dict() else: data = request.DATA - parent_id = kwargs['pk'] - sub_id = data.get('id', None) - main = self.__class__.parent_model.objects.get(pk=parent_id) - severable = getattr(self.__class__, 'severable', True) - subs = None + # add the key to the post data using the pk from the URL + data[inject_primary_key] = kwargs['pk'] - if sub_id: - subs = self.__class__.model.objects.filter(pk=sub_id) - else: - if 'disassociate' in data: - raise PermissionDenied() # ID is required to disassociate - else: + # attempt to deserialize the object + serializer = self.serializer_class(data=data) + if not serializer.is_valid(): + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) - # this is a little tricky and a little manual - # the object ID was not specified, so it probably doesn't exist in the DB yet. - # we want to see if we can create it. The URL may choose to inject it's primary key into the object - # because we are posting to a subcollection. Use all the normal access control mechanisms. + # Verify we have permission to add the object as given. + if not request.user.can_access(self.model, 'add', serializer.init_data): + raise PermissionDenied() - inject_primary_key = getattr(self.__class__, 'inject_primary_key_on_post_as', None) + # save the object through the serializer, reload and returned the saved object deserialized + obj = serializer.save() + serializer = self.serializer_class(obj) - if inject_primary_key is not None: + return Response(serializer.data, status=status.HTTP_201_CREATED) - # add the key to the post data using the pk from the URL - data[inject_primary_key] = kwargs['pk'] + def attach(self, request, *args, **kwargs): + created = False + parent = self.get_parent_object() + relationship = getattr(parent, self.relationship) + sub_id = request.DATA.get('id', None) + data = request.DATA - # attempt to deserialize the object - ser = self.__class__.serializer_class(data=data) - if not ser.is_valid(): - return Response(status=status.HTTP_400_BAD_REQUEST, data=ser.errors) + # Create the sub object if an ID is not provided. + if not sub_id: + response = self.create(request, *args, **kwargs) + if response.status_code != status.HTTP_201_CREATED: + return response + sub_id = response.data['id'] + data = response.data + created = True - if not check_user_access(request.user, self.model, 'add', ser.init_data): - raise PermissionDenied() + # Retrive the sub object (whether created or by ID). + try: + sub = self.model.objects.get(pk=sub_id) + except self.model.DoesNotExist: + data = dict(msg='Object with id %s cannot be found' % sub_id) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + # Verify we have permission to attach. + if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created): + raise PermissionDenied() - # save the object through the serializer, reload and returned the saved object deserialized - obj = ser.save() - ser = self.__class__.serializer_class(obj) - - # 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: - - # FIXME: refactor into smaller functions - - if obj.__class__ in [ User]: - if self.__class__.parent_model == Organization: - # user can't inject an organization because it's not part of the user - # model so we have to cheat here. This may happen for other cases - # where we are creating a user immediately on a subcollection - # when that user does not already exist. Relations will work post-save. - organization = Organization.objects.get(pk=data[inject_primary_key]) - if not request.user.is_superuser: - if not organization.admins.filter(pk=request.user.pk).count() > 0: - raise PermissionDenied() - else: - raise NotImplementedError() - else: - if not check_user_access(request.user, type(obj), 'read', obj): - raise PermissionDenied() - # If we just created a new object, we may not yet be able to read it because it's not yet associated with its parent model. - if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data, skip_sub_obj_read_check=True): - raise PermissionDenied() - - # FIXME: manual attachment code neccessary for users here, move this into the main code. - # this is because users don't have FKs into what they are attaching. (also refactor) - - if self.__class__.parent_model == Organization: - organization = Organization.objects.get(pk=data[inject_primary_key]) - import awx.main.views - if self.__class__ == awx.main.views.OrganizationUsersList: - organization.users.add(obj) - elif self.__class__ == awx.main.views.OrganizationAdminsList: - organization.admins.add(obj) - - else: - if not check_user_access(request.user, type(obj), 'read', obj): - raise PermissionDenied() - # FIXME: should generalize this - if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data): - raise PermissionDenied() - - # why are we returning here? - # return Response(status=status.HTTP_201_CREATED, data=ser.data) - created = True - subs = [ obj ] - - else: - - # view didn't specify a way to get the pk from the URL, so not even trying - return Response(status=status.HTTP_400_BAD_REQUEST, data=json.dumps(dict(msg='object cannot be created'))) - - # we didn't have to create the object, so this is just associating the two objects together now... - # (or disassociating them) - - if len(subs) != 1: - return Response(status=status.HTTP_400_BAD_REQUEST) - sub = subs[0] - relationship = getattr(main, self.__class__.relationship) - - if not 'disassociate' in data: - if not request.user.is_superuser: - if type(main) != User: - #if not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship, data): - if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data): - raise PermissionDenied() - else: - #if not UserHelper.can_user_attach(request.user, main, sub, self.__class__.relationship, data): - if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data): - raise PermissionDenied() - - if sub not in relationship.all(): - relationship.add(sub) - else: - if not request.user.is_superuser: - if type(main) != User: - #if not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship): - if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship): - raise PermissionDenied() - else: - #if not UserHelper.can_user_unattach(request.user, main, sub, self.__class__.relationship): - if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship): - raise PermissionDenied() - - - if severable: - relationship.remove(sub) - else: - # resource is just a ForeignKey, can't remove it from the set, just set it inactive - sub.mark_inactive() - - - # verify we didn't add anything to it's own children - if type(main) == Group: - all_children = main.get_all_children().all() - if main in all_children: - # no attaching to child objects (in the case of groups) - raise PermissionDenied() + # Attach the object to the collection. + if sub not in relationship.all(): + relationship.add(sub) if created: - return Response(status=status.HTTP_201_CREATED, data=ser.data) + return Response(data, status=status.HTTP_201_CREATED) else: return Response(status=status.HTTP_204_NO_CONTENT) + def unattach(self, request, *args, **kwargs): + sub_id = request.DATA.get('id', None) + if not sub_id: + data = dict(msg='"id" is required to disassociate') + return Response(data, status=status.HTTP_400_BAD_REQUEST) -class BaseDetail(generics.RetrieveUpdateDestroyAPIView): + parent = self.get_parent_object() + severable = getattr(self, 'severable', True) + relationship = getattr(parent, self.relationship) + + try: + sub = self.model.objects.get(pk=sub_id) + except self.model.DoesNotExist: + data = dict(msg='Object with id %s cannot be found' % sub_id) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship): + raise PermissionDenied() + + if severable: + relationship.remove(sub) + else: + # resource is just a ForeignKey, can't remove it from the set, just set it inactive + sub.mark_inactive() + + return Response(status=status.HTTP_204_NO_CONTENT) + + def post(self, request, *args, **kwargs): + if 'disassociate' in request.DATA: + return self.unattach(request, *args, **kwargs) + else: + return self.attach(request, *args, **kwargs) + + +class RetrieveAPIView(generics.RetrieveAPIView): + pass + +class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestroyAPIView): def pre_save(self, obj): if type(obj) not in [ User ]: @@ -237,23 +204,18 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView): raise Http404() if getattr(obj, 'is_active', True) == False: raise Http404() - #if not request.user.is_superuser and not self.delete_permissions_check(request, obj): - if not check_user_access(request.user, self.model, 'delete', obj): + if not request.user.can_access(self.model, 'delete', obj): raise PermissionDenied() - if isinstance(obj, PrimordialModel): + if hasattr(obj, 'mark_inactive'): obj.mark_inactive() - elif type(obj) == User: - obj.username = "_deleted_%s_%s" % (now().isoformat(), obj.username) - obj.is_active = False - obj.save() else: raise Exception("InternalError: destroy() not implemented yet for %s" % obj) return HttpResponse(status=204) - def put(self, request, *args, **kwargs): - self.put_filter(request, *args, **kwargs) - return super(BaseDetail, self).put(request, *args, **kwargs) + def update(self, request, *args, **kwargs): + self.update_filter(request, *args, **kwargs) + return super(RetrieveUpdateDestroyAPIView, self).update(request, *args, **kwargs) - def put_filter(self, request, *args, **kwargs): - ''' scrub any fields the user cannot/should not put, based on user context. This runs after read-only serialization filtering ''' + def update_filter(self, request, *args, **kwargs): + ''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering ''' pass diff --git a/awx/main/compat.py b/awx/main/compat.py index 3e4955b26d..dae7e85e25 100644 --- a/awx/main/compat.py +++ b/awx/main/compat.py @@ -1,3 +1,6 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + ''' Compability library for support of both Django 1.4.x and Django 1.5.x. ''' diff --git a/awx/main/custom_filters.py b/awx/main/filters.py similarity index 92% rename from awx/main/custom_filters.py rename to awx/main/filters.py index 234910183d..6f7eb78d1f 100644 --- a/awx/main/custom_filters.py +++ b/awx/main/filters.py @@ -1,10 +1,10 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. +# Django REST Framework from rest_framework.filters import BaseFilterBackend -from django.core.exceptions import PermissionDenied -class CustomFilterBackend(object): +class DefaultFilterBackend(BaseFilterBackend): def filter_queryset(self, request, queryset, view): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index daab3fac03..8b967a967e 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -33,6 +33,10 @@ from djcelery.models import TaskMeta # Django-REST-Framework from rest_framework.authtoken.models import Token +__all__ = ['Organization', 'Team', 'Project', 'Credential', 'Inventory', + 'Host', 'Group', 'Permission', 'JobTemplate', 'Job', + 'JobHostSummary', 'JobEvent'] + # TODO: reporting model TBD PERM_INVENTORY_ADMIN = 'admin' @@ -1213,6 +1217,21 @@ class JobEvent(models.Model): # TODO: reporting (MPD) +# Add mark_inactive method to User model. +def user_mark_inactive(user, save=True): + '''Use instead of delete to rename and mark users inactive.''' + if user.is_active: + user.username = "_deleted_%s_%s" % (now().isoformat(), user.username) + user.is_active = False + if save: + user.save() +User.add_to_class('mark_inactive', user_mark_inactive) + +# Add custom methods to User model for permissions checks. +from awx.main.access import * +User.add_to_class('get_queryset', get_user_queryset) +User.add_to_class('can_access', check_user_access) + @receiver(post_save, sender=User) def create_auth_token_for_user(sender, **kwargs): instance = kwargs.get('instance', None) diff --git a/awx/main/rbac.py b/awx/main/permissions.py similarity index 77% rename from awx/main/rbac.py rename to awx/main/permissions.py index 3db7c6aeaf..a4c1071c3b 100644 --- a/awx/main/rbac.py +++ b/awx/main/permissions.py @@ -1,26 +1,38 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. +# Python import logging + +# Django from django.http import Http404 + +# Django REST Framework from rest_framework.exceptions import PermissionDenied from rest_framework import permissions + +# AWX from awx.main.access import * from awx.main.models import * -logger = logging.getLogger('awx.main.rbac') +logger = logging.getLogger('awx.main.permissions') -# FIXME: this will probably need to be subclassed by object type +__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', + 'JobTaskPermission'] -class CustomRbac(permissions.BasePermission): +class ModelAccessPermission(permissions.BasePermission): + ''' + Default permissions class to check user access based on the model and + request method, optionally verifying the request data. + ''' - def _check_options_permissions(self, request, view, obj=None): - return self._check_get_permissions(request, view, obj) + def check_options_permissions(self, request, view, obj=None): + return self.check_get_permissions(request, view, obj) - def _check_head_permissions(self, request, view, obj=None): - return self._check_get_permissions(request, view, obj) + def check_head_permissions(self, request, view, obj=None): + return self.check_get_permissions(request, view, obj) - def _check_get_permissions(self, request, view, obj=None): + def check_get_permissions(self, request, view, obj=None): if hasattr(view, 'parent_model'): parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk']) if not check_user_access(request.user, view.parent_model, 'read', @@ -30,7 +42,7 @@ class CustomRbac(permissions.BasePermission): return True return check_user_access(request.user, view.model, 'read', obj) - def _check_post_permissions(self, request, view, obj=None): + def check_post_permissions(self, request, view, obj=None): if hasattr(view, 'parent_model'): parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk']) return True @@ -39,7 +51,7 @@ class CustomRbac(permissions.BasePermission): return True return check_user_access(request.user, view.model, 'add', request.DATA) - def _check_put_permissions(self, request, view, obj=None): + def check_put_permissions(self, request, view, obj=None): if not obj: return True # FIXME: For some reason this needs to return True # because it is first called with obj=None? @@ -50,13 +62,20 @@ class CustomRbac(permissions.BasePermission): return check_user_access(request.user, view.model, 'change', obj, request.DATA) - def _check_delete_permissions(self, request, view, obj=None): + def check_patch_permissions(self, request, view, obj=None): + return self.check_put_permissions(request, view, obj) + + def check_delete_permissions(self, request, view, obj=None): if not obj: return True # FIXME: For some reason this needs to return True # because it is first called with obj=None? return check_user_access(request.user, view.model, 'delete', obj) - def _check_permissions(self, request, view, obj=None): + def check_permissions(self, request, view, obj=None): + ''' + Perform basic permissions checking before delegating to the appropriate + method based on the request method. + ''' # Check that obj (if given) is active, otherwise raise a 404. active = getattr(obj, 'active', getattr(obj, 'is_active', True)) @@ -79,39 +98,20 @@ class CustomRbac(permissions.BasePermission): # Check permissions for the given view and object, based on the request # method used. - check_method = getattr(self, '_check_%s_permissions' % \ + check_method = getattr(self, 'check_%s_permissions' % \ request.method.lower(), None) result = check_method and check_method(request, view, obj) if not result: raise PermissionDenied() return result - - # If no obj is given, check list permissions. - - if obj is None: - if getattr(view, 'list_permissions_check', None): - if not view.list_permissions_check(request): - raise PermissionDenied() - elif not getattr(view, 'item_permissions_check', None): - raise Exception('internal error, list_permissions_check or ' - 'item_permissions_check must be defined') - return True - - # Otherwise, check the item permissions for the given obj. - - else: - if not view.item_permissions_check(request, obj): - raise PermissionDenied() - return True def has_permission(self, request, view, obj=None): - logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)', request.user, request.method, request.DATA, view.__class__.__name__, obj) try: - response = self._check_permissions(request, view, obj) + response = self.check_permissions(request, view, obj) except Exception, e: logger.debug('has_permission raised %r', e, exc_info=True) raise @@ -122,7 +122,7 @@ class CustomRbac(permissions.BasePermission): def has_object_permission(self, request, view, obj): return self.has_permission(request, view, obj) -class JobTemplateCallbackPermission(CustomRbac): +class JobTemplateCallbackPermission(ModelAccessPermission): ''' Permission check used by job template callback view for requests from empheral hosts. @@ -149,7 +149,7 @@ class JobTemplateCallbackPermission(CustomRbac): else: return True -class JobTaskPermission(CustomRbac): +class JobTaskPermission(ModelAccessPermission): ''' Permission checks used for API callbacks from running a task. ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f7d1222d06..a566d29d7d 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1,19 +1,26 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. +# Python import cStringIO import json import logging import os -import select import subprocess import tempfile -import time import traceback -from celery import Task -from django.conf import settings + +# Pexpect import pexpect -from awx.main.models import * + +# Celery +from celery import Task + +# Django +from django.conf import settings + +# AWX +from awx.main.models import Job __all__ = ['RunJob'] @@ -39,7 +46,6 @@ class RunJob(Task): if field == 'status': update_fields.append('failed') job.save(update_fields=update_fields) - # FIXME: Commit transaction? return job def get_path_to(self, *args): @@ -165,6 +171,7 @@ class RunJob(Task): r'Bad passphrase, try again for .*:', r'sudo password.*:', r'SSH password:', + r'Password:', pexpect.TIMEOUT, pexpect.EOF, ] @@ -175,7 +182,7 @@ class RunJob(Task): child.sendline('') elif result_id == 2: child.sendline(passwords.get('sudo_password', '')) - elif result_id == 3: + elif result_id in (3, 4): child.sendline(passwords.get('ssh_password', '')) job_updates = {} if logfile_pos != logfile.tell(): diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index d3c0d2bfcc..656f8fc554 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -187,9 +187,7 @@ class CleanupDeletedTest(BaseCommandTest): self.assertEqual(counts_before, counts_after) # "Delete some users". for user in User.objects.all(): - user.username = "_deleted_%s_%s" % (now().isoformat(), user.username) - user.is_active = False - user.save() + user.mark_inactive() # With days=1, no users will be deleted. counts_before = self.get_user_counts() self.assertTrue(counts_before[1]) diff --git a/awx/main/views.py b/awx/main/views.py index 7023dc4732..813e0594c6 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -19,19 +19,18 @@ from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.exceptions import PermissionDenied from rest_framework import generics from rest_framework.parsers import YAMLParser -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.renderers import YAMLRenderer from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView # AWX -from awx.main.access import * from awx.main.authentication import JobTaskAuthentication from awx.main.licenses import LicenseReader from awx.main.base_views import * from awx.main.models import * -from awx.main.rbac import * +from awx.main.permissions import * from awx.main.serializers import * def handle_error(request, status=404): @@ -59,6 +58,7 @@ class ApiRootView(APIView): information about the available API versions. ''' + permission_classes = (AllowAny,) view_name = 'REST API' def get(self, request, format=None): @@ -81,6 +81,7 @@ class ApiV1RootView(APIView): Subject to change until the final 1.2 release. ''' + permission_classes = (AllowAny,) view_name = 'Version 1' def get(self, request, format=None): @@ -162,297 +163,140 @@ class AuthTokenView(ObtainAuthToken): Authenticate: Token 8f17825cf08a7efea124f2638f3896f6637f8745 ''' + permission_classes = (AllowAny,) renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES -class OrganizationList(BaseList): +class OrganizationList(ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - # I can see the organizations if: - # I am a superuser - # I am an admin of the organization - # I am a member of the organization - - def get_queryset(self): - ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' - base = Organization.objects - if self.request.user.is_superuser: - return base.all() - return base.filter( - admins__in = [ self.request.user ] - ).distinct() | base.filter( - users__in = [ self.request.user ] - ).distinct() - -class OrganizationDetail(BaseDetail): +class OrganizationDetail(RetrieveUpdateDestroyAPIView): model = Organization serializer_class = OrganizationSerializer - permission_classes = (CustomRbac,) -class OrganizationInventoriesList(BaseSubList): +class OrganizationInventoriesList(SubListAPIView): model = Inventory serializer_class = InventorySerializer - permission_classes = (CustomRbac,) parent_model = Organization relationship = 'inventories' - postable = False - def get_queryset(self): - ''' to list inventories in the organization, I must be a superuser or org admin ''' - organization = Organization.objects.get(pk=self.kwargs['pk']) - if not (self.request.user.is_superuser or self.request.user in organization.admins.all()): - # FIXME: use: organization.can_user_administrate(...) ? - raise PermissionDenied() - return Inventory.objects.filter(organization__in=[organization]) - -class OrganizationUsersList(BaseSubList): +class OrganizationUsersList(SubListCreateAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) parent_model = Organization relationship = 'users' - postable = True inject_primary_key_on_post_as = 'organization' - filter_fields = ('username',) - def get_queryset(self): - ''' to list users in the organization, I must be a superuser or org admin ''' - organization = Organization.objects.get(pk=self.kwargs['pk']) - if not self.request.user.is_superuser and not self.request.user in organization.admins.all(): - raise PermissionDenied() - return User.objects.filter(organizations__in = [ organization ]) - -class OrganizationAdminsList(BaseSubList): +class OrganizationAdminsList(SubListCreateAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) parent_model = Organization relationship = 'admins' - postable = True inject_primary_key_on_post_as = 'organization' - filter_fields = ('username',) - def get_queryset(self): - ''' to list admins in the organization, I must be a superuser or org admin ''' - organization = Organization.objects.get(pk=self.kwargs['pk']) - if not self.request.user.is_superuser and not self.request.user in organization.admins.all(): - raise PermissionDenied() - return User.objects.filter(admin_of_organizations__in = [ organization ]) -class OrganizationProjectsList(BaseSubList): +class OrganizationProjectsList(SubListCreateAPIView): model = Project serializer_class = ProjectSerializer - permission_classes = (CustomRbac,) - parent_model = Organization # for sub list - relationship = 'projects' # " " - postable = True + parent_model = Organization + relationship = 'projects' inject_primary_key_on_post_as = 'organization' - filter_fields = ('name',) - def get_queryset(self): - ''' to list projects in the organization, I must be a superuser or org admin ''' - organization = Organization.objects.get(pk=self.kwargs['pk']) - if not (self.request.user.is_superuser or self.request.user in organization.admins.all()): - raise PermissionDenied() - return Project.objects.filter(organizations__in = [ organization ]) - -class OrganizationTeamsList(BaseSubList): +class OrganizationTeamsList(SubListCreateAPIView): model = Team serializer_class = TeamSerializer - permission_classes = (CustomRbac,) parent_model = Organization relationship = 'teams' - postable = True inject_primary_key_on_post_as = 'organization' severable = False - filter_fields = ('name',) - def get_queryset(self): - ''' to list users in the organization, I must be a superuser or org admin ''' - organization = Organization.objects.get(pk=self.kwargs['pk']) - if not self.request.user.is_superuser and not self.request.user in organization.admins.all(): - raise PermissionDenied() - return Team.objects.filter(organization = organization) - -class TeamList(BaseList): +class TeamList(ListCreateAPIView): model = Team serializer_class = TeamSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - # I can see a team if: - # I am a superuser - # I am an admin of the organization that the team is - # I am on that team - - def get_queryset(self): - ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' - base = Team.objects - if self.request.user.is_superuser: - return base.all() - return base.filter( - organization__admins__in = [ self.request.user ] - ).distinct() | base.filter( - users__in = [ self.request.user ] - ).distinct() - -class TeamDetail(BaseDetail): +class TeamDetail(RetrieveUpdateDestroyAPIView): model = Team serializer_class = TeamSerializer - permission_classes = (CustomRbac,) -class TeamUsersList(BaseSubList): +class TeamUsersList(SubListCreateAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) parent_model = Team relationship = 'users' - postable = True inject_primary_key_on_post_as = 'team' severable = True - filter_fields = ('username',) - def get_queryset(self): - # FIXME: audit all BaseSubLists to check for permissions on the original object too - 'team members can see the whole team, as can org admins or superusers' - team = Team.objects.get(pk=self.kwargs['pk']) - base = team.users.all() - if self.request.user.is_superuser or self.request.user in team.organization.admins.all(): - return base - if self.request.user in team.users.all(): - return base - raise PermissionDenied() - -class TeamPermissionsList(BaseSubList): +class TeamPermissionsList(SubListCreateAPIView): model = Permission serializer_class = PermissionSerializer - permission_classes = (CustomRbac,) parent_model = Team relationship = 'permissions' - postable = True - filter_fields = ('name',) inject_primary_key_on_post_as = 'team' def get_queryset(self): + # FIXME team = Team.objects.get(pk=self.kwargs['pk']) base = Permission.objects.filter(team = team) #if Team.can_user_administrate(self.request.user, team, None): - if check_user_access(self.request.user, Team, 'change', team, None): + if self.request.user.can_access(Team, 'change', team, None): return base elif team.users.filter(pk=self.request.user.pk).count() > 0: return base raise PermissionDenied() - -class TeamProjectsList(BaseSubList): +class TeamProjectsList(SubListCreateAPIView): model = Project serializer_class = ProjectSerializer - permission_classes = (CustomRbac,) parent_model = Team relationship = 'projects' - postable = True inject_primary_key_on_post_as = 'team' severable = True - # FIXME: filter_fields is no longer used, think we can remove these references everywhere given new custom filtering -- MPD - filter_fields = ('name',) - - def get_queryset(self): - team = Team.objects.get(pk=self.kwargs['pk']) - base = team.projects.all() - if self.request.user.is_superuser or self.request.user in team.organization.admins.all(): - return base - if self.request.user in team.users.all(): - return base - raise PermissionDenied() - - -class TeamCredentialsList(BaseSubList): +class TeamCredentialsList(SubListCreateAPIView): model = Credential serializer_class = CredentialSerializer - permission_classes = (CustomRbac,) parent_model = Team relationship = 'credentials' - postable = True inject_primary_key_on_post_as = 'team' - filter_fields = ('name',) - def get_queryset(self): - team = Team.objects.get(pk=self.kwargs['pk']) - #if not Team.can_user_administrate(self.request.user, team, None): - if not check_user_access(self.request.user, Team, 'change', team, None): - if not (self.request.user.is_superuser or self.request.user in team.users.all()): - raise PermissionDenied() - project_credentials = Credential.objects.filter( - team = team - ) - return project_credentials.distinct() - - -class ProjectList(BaseList): +class ProjectList(ListCreateAPIView): model = Project serializer_class = ProjectSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - # I can see a project if - # I am a superuser - # I am an admin of the organization that contains the project - # I am a member of a team that also contains the project - - def get_queryset(self): - ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' - base = Project.objects - if self.request.user.is_superuser: - return base.all() - my_teams = Team.objects.filter(users__in = [ self.request.user]) - my_orgs = Organization.objects.filter(admins__in = [ self.request.user ]) - return base.filter( - teams__in = my_teams - ).distinct() | base.filter( - organizations__in = my_orgs - ).distinct() - -class ProjectDetail(BaseDetail): +class ProjectDetail(RetrieveUpdateDestroyAPIView): model = Project serializer_class = ProjectSerializer - permission_classes = (CustomRbac,) -class ProjectDetailPlaybooks(generics.RetrieveAPIView): +class ProjectDetailPlaybooks(RetrieveAPIView): model = Project serializer_class = ProjectPlaybooksSerializer - permission_classes = (CustomRbac,) -class ProjectOrganizationsList(BaseSubList): +class ProjectOrganizationsList(SubListAPIView): model = Organization serializer_class = OrganizationSerializer - permission_classes = (CustomRbac,) parent_model = Project relationship = 'organizations' - postable = True inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work? - filter_fields = ('name',) def get_queryset(self): + # FIXME project = Project.objects.get(pk=self.kwargs['pk']) if not self.request.user.is_superuser: raise PermissionDenied() @@ -462,12 +306,9 @@ class ProjectTeamsList(BaseSubList): model = Team serializer_class = TeamSerializer - permission_classes = (CustomRbac,) parent_model = Project relationship = 'teams' - postable = True inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work? - filter_fields = ('name',) def get_queryset(self): project = Project.objects.get(pk=self.kwargs['pk']) @@ -475,167 +316,90 @@ class ProjectTeamsList(BaseSubList): raise PermissionDenied() return Team.objects.filter(projects__in = [ project ]) -class UserList(BaseList): +class UserList(ListCreateAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) - filter_fields = ('username',) - def post(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs): password = request.DATA.get('password', None) - result = super(UserList, self).post(request, *args, **kwargs) + response = super(UserList, self).create(request, *args, **kwargs) if password: - pk = result.data['id'] + pk = response.data['id'] user = User.objects.get(pk=pk) user.set_password(password) user.save() - return result + return response - def get_queryset(self): - ''' I can see user records when I'm a superuser, I'm that user, I'm their org admin, or I'm on a team with that user ''' - base = User.objects - if self.request.user.is_superuser: - return base.all() - mine = base.filter(pk = self.request.user.pk).distinct() - admin_of = base.filter(organizations__in = self.request.user.admin_of_organizations.all()).distinct() - same_team = base.filter(teams__in = self.request.user.teams.all()).distinct() - return mine | admin_of | same_team - -class UserMeList(BaseList): +class UserMeList(ListAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) - filter_fields = ('username',) view_name = 'Me!' - def post(self, request, *args, **kwargs): - raise PermissionDenied() - def get_queryset(self): - ''' a quick way to find my user record ''' - return User.objects.filter(pk=self.request.user.pk) + return self.model.objects.filter(pk=self.request.user.pk) -class UserTeamsList(BaseSubList): +class UserTeamsList(SubListAPIView): model = Team serializer_class = TeamSerializer - permission_classes = (CustomRbac,) parent_model = User relationship = 'teams' - postable = False - filter_fields = ('name',) - def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - return Team.objects.filter(users__in = [ user ]) - -class UserPermissionsList(BaseSubList): +class UserPermissionsList(SubListCreateAPIView): model = Permission serializer_class = PermissionSerializer - permission_classes = (CustomRbac,) parent_model = User relationship = 'permissions' - postable = True - filter_fields = ('name',) inject_primary_key_on_post_as = 'user' - def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - return Permission.objects.filter(user=user) - -class UserProjectsList(BaseSubList): +class UserProjectsList(SubListAPIView): model = Project serializer_class = ProjectSerializer - permission_classes = (CustomRbac,) parent_model = User - relationship = 'teams' - postable = False - filter_fields = ('name',) + relationship = 'projects' def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - teams = user.teams.all() - return Project.objects.filter(teams__in = teams) + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs.filter(teams__in=parent.teams.distinct()) -class UserCredentialsList(BaseSubList): +class UserCredentialsList(SubListCreateAPIView): model = Credential serializer_class = CredentialSerializer - permission_classes = (CustomRbac,) parent_model = User relationship = 'credentials' - postable = True inject_primary_key_on_post_as = 'user' - filter_fields = ('name',) - def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - project_credentials = Credential.objects.filter( - team__users__in = [ user ] - ) - return user.credentials.distinct() | project_credentials.distinct() - -class UserOrganizationsList(BaseSubList): +class UserOrganizationsList(SubListAPIView): model = Organization serializer_class = OrganizationSerializer - permission_classes = (CustomRbac,) parent_model = User relationship = 'organizations' - postable = False - filter_fields = ('name',) - def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - return Organization.objects.filter(users__in = [ user ]) - -class UserAdminOfOrganizationsList(BaseSubList): +class UserAdminOfOrganizationsList(SubListAPIView): model = Organization serializer_class = OrganizationSerializer - permission_classes = (CustomRbac,) parent_model = User relationship = 'admin_of_organizations' - postable = False - filter_fields = ('name',) - def get_queryset(self): - user = User.objects.get(pk=self.kwargs['pk']) - #if not UserHelper.can_user_administrate(self.request.user, user, None): - if not check_user_access(self.request.user, User, 'change', user, None): - raise PermissionDenied() - return Organization.objects.filter(admins__in = [ user ]) - -class UserDetail(BaseDetail): +class UserDetail(RetrieveUpdateDestroyAPIView): model = User serializer_class = UserSerializer - permission_classes = (CustomRbac,) - def put_filter(self, request, *args, **kwargs): + def update_filter(self, request, *args, **kwargs): ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' obj = User.objects.get(pk=kwargs['pk']) - can_admin = check_user_access(request.user, User, 'admin', obj, request.DATA) + can_admin = request.user.can_access(User, 'admin', obj, request.DATA) if not can_admin or can_admin == 'partial': admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active', 'is_superuser') @@ -653,358 +417,155 @@ class UserDetail(BaseDetail): obj.save() request.DATA.pop('password') -class CredentialList(BaseList): +class CredentialList(ListAPIView): model = Credential serializer_class = CredentialSerializer - permission_classes = (CustomRbac,) - postable = False - def get_queryset(self): - return get_user_queryset(self.request.user, self.model) - -class CredentialDetail(BaseDetail): +class CredentialDetail(RetrieveUpdateDestroyAPIView): model = Credential serializer_class = CredentialSerializer - permission_classes = (CustomRbac,) -class PermissionDetail(BaseDetail): +class PermissionDetail(RetrieveUpdateDestroyAPIView): model = Permission serializer_class = PermissionSerializer - permission_classes = (CustomRbac,) -class InventoryList(BaseList): +class InventoryList(ListCreateAPIView): model = Inventory serializer_class = InventorySerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - def _filter_queryset(self, base): - if self.request.user.is_superuser: - return base.all() - admin_of = base.filter(organization__admins__in = [ self.request.user ]).distinct() - has_user_perms = base.filter( - permissions__user__in = [ self.request.user ], - permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, - ).distinct() - has_team_perms = base.filter( - permissions__team__in = self.request.user.teams.all(), - permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, - ).distinct() - return admin_of | has_user_perms | has_team_perms - - def get_queryset(self): - ''' I can see inventory when I'm a superuser, an org admin of the inventory, or I have permissions on it ''' - base = Inventory.objects - return self._filter_queryset(base) - -class InventoryDetail(BaseDetail): +class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventorySerializer - permission_classes = (CustomRbac,) -class HostList(BaseList): +class HostList(ListCreateAPIView): model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - def get_queryset(self): - ''' - I can see hosts when: - I'm a superuser, - or an organization admin of an inventory they are in - or when I have allowing read permissions via a user or team on an inventory they are in - ''' - base = Host.objects - 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 HostDetail(BaseDetail): +class HostDetail(RetrieveUpdateDestroyAPIView): model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) -class InventoryHostsList(BaseSubList): +class InventoryHostsList(SubListCreateAPIView): model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) - # to allow the sub-aspect listing parent_model = Inventory relationship = 'hosts' - # to allow posting to this resource to create resources - postable = True - # FIXME: go back and add these to other SubLists + parent_access = 'read' inject_primary_key_on_post_as = 'inventory' severable = False - filter_fields = ('name',) - def get_queryset(self): - inventory = Inventory.objects.get(pk=self.kwargs['pk']) - base = inventory.hosts - # FIXME: verify that you can can_read permission on the inventory is required - return base.all() - -class HostGroupsList(BaseSubList): +class HostGroupsList(SubListCreateAPIView): ''' the list of groups a host is directly a member of ''' model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) parent_model = Host relationship = 'groups' - postable = True + parent_access = 'read' inject_primary_key_on_post_as = 'host' - filter_fields = ('name',) - def get_queryset(self): - - parent = Host.objects.get(pk=self.kwargs['pk']) - - # FIXME: verify read permissions on this object are still required at a higher level - - base = parent.groups - 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 HostAllGroupsList(BaseSubList): +class HostAllGroupsList(SubListAPIView): ''' the list of all groups of which the host is directly or indirectly a member ''' model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) parent_model = Host relationship = 'groups' - filter_fields = ('name',) + parent_access = 'read' def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + sublist_qs = parent.all_groups.distinct() + return qs & sublist_qs - parent = Host.objects.get(pk=self.kwargs['pk']) - - # FIXME: verify read permissions on this object are still required at a higher level - - base = parent.all_groups - - 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 GroupList(BaseList): +class GroupList(ListCreateAPIView): model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - def get_queryset(self): - ''' - I can see groups when: - I'm a superuser, - or an organization admin of an inventory they are in - or when I have allowing read permissions via a user or team on an inventory they are in - ''' - base = Group.objects - 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 GroupChildrenList(BaseSubList): +class GroupChildrenList(SubListCreateAPIView): model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) parent_model = Group relationship = 'children' - postable = True + parent_access = 'read' inject_primary_key_on_post_as = 'parent' - filter_fields = ('name',) - def get_queryset(self): - - # FIXME: this is the mostly the same as GroupsList, share code similar to how done with Host and Group objects. - - parent = Group.objects.get(pk=self.kwargs['pk']) - - # FIXME: verify read permissions on this object are still required at a higher level - - base = parent.children - 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 GroupHostsList(BaseSubList): +class GroupHostsList(SubListCreateAPIView): ''' the list of hosts directly below a group ''' model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) parent_model = Group relationship = 'hosts' - postable = True + parent_access = 'read' inject_primary_key_on_post_as = 'group' - filter_fields = ('name',) - 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 GroupAllHostsList(BaseSubList): +class GroupAllHostsList(SubListAPIView): ''' the list of all hosts below a group, even including subgroups ''' model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) parent_model = Group relationship = 'hosts' - filter_fields = ('name',) + parent_access = 'read' def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + sublist_qs = parent.all_hosts.distinct() + return qs & sublist_qs - parent = Group.objects.get(pk=self.kwargs['pk']) - - # FIXME: verify read permissions on this object are still required at a higher level - - base = parent.all_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 GroupDetail(BaseDetail): +class GroupDetail(RetrieveUpdateDestroyAPIView): model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) -class InventoryGroupsList(BaseSubList): +class InventoryGroupsList(SubListCreateAPIView): model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) - # to allow the sub-aspect listing parent_model = Inventory relationship = 'groups' - # to allow posting to this resource to create resources - postable = True - # FIXME: go back and add these to other SubLists + parent_access = 'read' inject_primary_key_on_post_as = 'inventory' severable = False - filter_fields = ('name',) - def get_queryset(self): - # FIXME: share code with inventory filter queryset methods (make that a classmethod) - inventory = Inventory.objects.get(pk=self.kwargs['pk']) - base = inventory.groups - # FIXME: verify that you can can_read permission on the inventory is required - return base - -class InventoryRootGroupsList(BaseSubList): +class InventoryRootGroupsList(SubListCreateAPIView): model = Group serializer_class = GroupSerializer - permission_classes = (CustomRbac,) parent_model = Inventory relationship = 'groups' - postable = True + parent_access = 'read' inject_primary_key_on_post_as = 'inventory' severable = False - filter_fields = ('name',) def get_queryset(self): - inventory = Inventory.objects.get(pk=self.kwargs['pk']) - base = inventory.groups - all_ids = base.values_list('id', flat=True) - return base.exclude(parents__pk__in = all_ids) + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + all_pks = parent.groups.values_list('pk', flat=True) + sublist_qs = parent.groups.exclude(parents__pk__in=all_pks).distinct() + return qs & sublist_qs -class BaseVariableDetail(BaseDetail): +class BaseVariableDetail(RetrieveUpdateDestroyAPIView): - permission_classes = (CustomRbac,) parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser] renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer] - is_variable_data = True # Special flag for RBAC + is_variable_data = True # Special flag for permissions check. class InventoryVariableDetail(BaseVariableDetail): @@ -1021,7 +582,7 @@ class GroupVariableDetail(BaseVariableDetail): model = Group serializer_class = GroupVariableDataSerializer -class InventoryScriptView(generics.RetrieveAPIView): +class InventoryScriptView(RetrieveAPIView): ''' Return inventory group and host data as needed for an inventory script. @@ -1042,11 +603,9 @@ class InventoryScriptView(generics.RetrieveAPIView): self.object = self.get_object() hostname = request.QUERY_PARAMS.get('host', '') if hostname: - try: - host = self.object.hosts.get(active=True, name=hostname) - data = host.variables_dict - except Host.DoesNotExist: - raise Http404 + host = get_object_or_404(self.object.hosts, active=True, + name=hostname) + data = host.variables_dict else: data = {} for group in self.object.groups.filter(active=True): @@ -1069,23 +628,20 @@ class InventoryScriptView(generics.RetrieveAPIView): } return Response(data) -class JobTemplateList(BaseList): +class JobTemplateList(ListCreateAPIView): model = JobTemplate serializer_class = JobTemplateSerializer - permission_classes = (CustomRbac,) - filter_fields = ('name',) - def get_queryset(self): - return get_user_queryset(self.request.user, self.model) + def _get_queryset(self): + return self.request.user.get_queryset(self.model) -class JobTemplateDetail(BaseDetail): +class JobTemplateDetail(RetrieveUpdateDestroyAPIView): model = JobTemplate serializer_class = JobTemplateSerializer - permission_classes = (CustomRbac,) -class JobTemplateCallback(generics.RetrieveAPIView): +class JobTemplateCallback(generics.GenericAPIView): ''' The job template callback allows for empheral hosts to launch a new job. @@ -1208,60 +764,47 @@ class JobTemplateCallback(generics.RetrieveAPIView): if not matching_hosts: data = dict(msg='No matching host could be found!') # FIXME: Log! - return Response(data, status=400) + return Response(data, status=status.HTTP_400_BAD_REQUEST) elif len(matching_hosts) > 1: data = dict(msg='Multiple hosts matched the request!') # FIXME: Log! - return Response(data, status=400) + return Response(data, status=status.HTTP_400_BAD_REQUEST) else: host = list(matching_hosts)[0] if not job_template.can_start_without_user_input(): data = dict(msg='Cannot start automatically, user input required!') # FIXME: Log! - return Response(data, status=400) + return Response(data, status=status.HTTP_400_BAD_REQUEST) limit = ':'.join(filter(None, [job_template.limit, host.name])) job = job_template.create_job(limit=limit, launch_type='callback') result = job.start() if not result: data = dict(msg='Error starting job!') - return Response(data, status=400) + return Response(data, status=status.HTTP_400_BAD_REQUEST) else: - return Response(status=202) + return Response(status=status.HTTP_202_ACCEPTED) -class JobTemplateJobsList(BaseSubList): +class JobTemplateJobsList(SubListCreateAPIView): model = Job serializer_class = JobSerializer - permission_classes = (CustomRbac,) - # to allow the sub-aspect listing parent_model = JobTemplate relationship = 'jobs' - # to allow posting to this resource to create resources - postable = True - # FIXME: go back and add these to other SubLists inject_primary_key_on_post_as = 'job_template' severable = False - #filter_fields = ('name',) - def get_queryset(self): - # FIXME: Verify read permission on the job template. - job_template = get_object_or_404(JobTemplate, pk=self.kwargs['pk']) - return job_template.jobs - -class JobList(BaseList): +class JobList(ListCreateAPIView): model = Job serializer_class = JobSerializer - permission_classes = (CustomRbac,) - def get_queryset(self): + def _get_queryset(self): return self.model.objects.all() # FIXME -class JobDetail(BaseDetail): +class JobDetail(RetrieveUpdateDestroyAPIView): model = Job serializer_class = JobSerializer - permission_classes = (CustomRbac,) def update(self, request, *args, **kwargs): obj = self.get_object() @@ -1270,10 +813,9 @@ class JobDetail(BaseDetail): return self.http_method_not_allowed(request, *args, **kwargs) return super(JobDetail, self).update(request, *args, **kwargs) -class JobStart(generics.RetrieveAPIView): +class JobStart(generics.GenericAPIView): model = Job - permission_classes = (CustomRbac,) def get(self, request, *args, **kwargs): obj = self.get_object() @@ -1290,16 +832,15 @@ class JobStart(generics.RetrieveAPIView): result = obj.start(**request.DATA) if not result: data = dict(passwords_needed_to_start=obj.get_passwords_needed_to_start()) - return Response(data, status=400) + return Response(data, status=status.HTTP_400_BAD_REQUEST) else: - return Response(status=202) + return Response(status=status.HTTP_202_ACCEPTED) else: - return Response(status=405) + return self.http_method_not_allowed(request, *args, **kwargs) -class JobCancel(generics.RetrieveAPIView): +class JobCancel(generics.GenericAPIView): model = Job - permission_classes = (CustomRbac,) def get(self, request, *args, **kwargs): obj = self.get_object() @@ -1312,25 +853,20 @@ class JobCancel(generics.RetrieveAPIView): obj = self.get_object() if obj.can_cancel: result = obj.cancel() - return Response(status=202) + return Response(status=status.HTTP_202_ACCEPTED) else: - return Response(status=405) + return self.http_method_not_allowed(request, *args, **kwargs) -class BaseJobHostSummariesList(generics.ListAPIView): +class BaseJobHostSummariesList(SubListAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer - permission_classes = (CustomRbac,) parent_model = None # Subclasses must define this attribute. relationship = 'job_host_summaries' + parent_access = 'read' view_name = 'Job Host Summary List' - def get_queryset(self): - # FIXME: Verify read permission on the parent object and job. - parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) - return getattr(parent_obj, self.relationship) - class HostJobHostSummariesList(BaseJobHostSummariesList): parent_model = Host @@ -1343,71 +879,46 @@ class JobJobHostSummariesList(BaseJobHostSummariesList): parent_model = Job -# FIXME: Subclasses of XJobHostSummaryList for failed/successful/etc. - -class JobHostSummaryDetail(generics.RetrieveAPIView): +class JobHostSummaryDetail(RetrieveAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer - permission_classes = (CustomRbac,) -class JobEventList(BaseList): +class JobEventList(ListAPIView): model = JobEvent serializer_class = JobEventSerializer - permission_classes = (CustomRbac,) - def get_queryset(self): - return self.model.objects.distinct() # FIXME: Permissions? - -class JobEventDetail(generics.RetrieveAPIView): +class JobEventDetail(RetrieveAPIView): model = JobEvent serializer_class = JobEventSerializer - permission_classes = (CustomRbac,) -class JobEventChildrenList(generics.ListAPIView): +class JobEventChildrenList(SubListAPIView): model = JobEvent serializer_class = JobEventSerializer - permission_classes = (CustomRbac,) parent_model = JobEvent relationship = 'children' view_name = 'Job Event Children List' - def get_queryset(self): - # FIXME: Verify read permission on the parent object and job. - parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) - return getattr(parent_obj, self.relationship) - -class JobEventHostsList(generics.ListAPIView): +class JobEventHostsList(SubListAPIView): model = Host serializer_class = HostSerializer - permission_classes = (CustomRbac,) parent_model = JobEvent relationship = 'hosts' view_name = 'Job Event Hosts List' - def get_queryset(self): - # FIXME: Verify read permission on the parent object and job. - parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) - return getattr(parent_obj, self.relationship) - -class BaseJobEventsList(generics.ListAPIView): +class BaseJobEventsList(SubListAPIView): model = JobEvent serializer_class = JobEventSerializer - permission_classes = (CustomRbac,) parent_model = None # Subclasses must define this attribute. relationship = 'job_events' - - def get_queryset(self): - # FIXME: Verify read permission on the parent object and job. - parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) - return getattr(parent_obj, self.relationship).distinct() + parent_access = 'read' class HostJobEventsList(BaseJobEventsList): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7edd2d6015..2f6a9284b8 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -147,8 +147,11 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'awx.main.permissions.ModelAccessPermission', + ), 'DEFAULT_FILTER_BACKENDS': ( - 'awx.main.custom_filters.CustomFilterBackend', + 'awx.main.filters.DefaultFilterBackend', ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', @@ -323,7 +326,7 @@ LOGGING = { 'handlers': ['console', 'file', 'syslog'], 'level': 'DEBUG', }, - 'awx.main.rbac': { + 'awx.main.permissions': { 'handlers': ['null'], # Comment the line below to show lots of permissions logging. 'propagate': False,