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.

This commit is contained in:
Chris Church
2013-07-19 00:33:56 -04:00
parent 401317cf41
commit a6c767907e
11 changed files with 494 additions and 852 deletions

View File

@@ -1,15 +1,18 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import sys import sys
import logging import logging
# Django
from django.db.models import Q from django.db.models import Q
from django.contrib.auth.models import User from django.contrib.auth.models import User
# Django REST Framework
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from awx import MODE # AWX
from awx.main.models import * from awx.main.models import *
from awx.main.licenses import LicenseReader from awx.main.licenses import LicenseReader
@@ -63,6 +66,12 @@ def check_user_access(user, model_class, action, *args, **kwargs):
return False return False
class BaseAccess(object): 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 model = None
@@ -102,8 +111,7 @@ class BaseAccess(object):
return self.can_change(obj, None) return self.can_change(obj, None)
else: else:
return bool(self.can_change(obj, None) and return bool(self.can_change(obj, None) and
check_user_access(self.user, type(sub_obj), 'read', self.user.can_access(type(sub_obj), 'read', sub_obj))
sub_obj))
def can_unattach(self, obj, sub_obj, relationship): def can_unattach(self, obj, sub_obj, relationship):
return self.can_change(obj, None) return self.can_change(obj, None)
@@ -120,7 +128,7 @@ class UserAccess(BaseAccess):
return self.model.objects.filter(is_active=True).filter( return self.model.objects.filter(is_active=True).filter(
Q(pk=self.user.pk) | Q(pk=self.user.pk) |
Q(organizations__in=self.user.admin_of_organizations.all()) | 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() ).distinct()
def can_read(self, obj): def can_read(self, obj):
@@ -158,6 +166,14 @@ class OrganizationAccess(BaseAccess):
model = Organization 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): def can_read(self, obj):
return bool(self.can_change(obj, None) or return bool(self.can_change(obj, None) or
self.user in obj.users.all()) self.user in obj.users.all())
@@ -174,6 +190,23 @@ class InventoryAccess(BaseAccess):
model = Inventory 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): def _has_permission_types(self, obj, allowed):
if self.user.is_superuser: if self.user.is_superuser:
return True 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 ''' ''' 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 not sub_obj.can_user_read(user, sub_obj):
if sub_obj and not skip_sub_obj_read_check: 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 False
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
@@ -252,8 +285,29 @@ class HostAccess(BaseAccess):
model = Host 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): 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): def can_add(self, data):
@@ -264,7 +318,7 @@ class HostAccess(BaseAccess):
inventory = Inventory.objects.get(pk=data['inventory']) inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on 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: if not permissions_ok:
return False return False
@@ -286,31 +340,82 @@ class HostAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
# Checks for admin or change permission on inventory, controls whether # Checks for admin or change permission on inventory, controls whether
# the user can edit variable data. # 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): class GroupAccess(BaseAccess):
model = Group 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): 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): def can_add(self, data):
if not 'inventory' in data: if not 'inventory' in data:
return False return False
inventory = Inventory.objects.get(pk=data['inventory']) inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on 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): def can_change(self, obj, data):
# Checks for admin or change permission on inventory, controls whether # Checks for admin or change permission on inventory, controls whether
# the user can attach subgroups or edit variable data. # 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): class CredentialAccess(BaseAccess):
model = Credential 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): def can_read(self, obj):
return self.can_change(obj, None) return self.can_change(obj, None)
@@ -319,10 +424,10 @@ class CredentialAccess(BaseAccess):
return True return True
if 'user' in data: if 'user' in data:
user_obj = User.objects.get(pk=data['user']) 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: if 'team' in data:
team_obj = Team.objects.get(pk=data['team']) 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): def can_change(self, obj, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -347,6 +452,9 @@ class TeamAccess(BaseAccess):
model = Team model = Team
def get_queryset(self):
return self.model.objects.distinct() # FIXME
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
return True return True
@@ -378,6 +486,22 @@ class ProjectAccess(BaseAccess):
model = Project 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): def can_read(self, obj):
if self.can_change(obj, None): if self.can_change(obj, None):
return True return True
@@ -403,6 +527,9 @@ class PermissionAccess(BaseAccess):
model = Permission model = Permission
def get_queryset(self):
return self.model.objects.distinct() # FIXME
def can_read(self, obj): def can_read(self, obj):
# a permission can be seen by the assigned user or team # a permission can be seen by the assigned user or team
# or anyone who can administrate that permission # or anyone who can administrate that permission
@@ -524,6 +651,9 @@ class JobAccess(BaseAccess):
model = Job model = Job
def get_queryset(self):
return self.model.objects.distinct() # FIXME
def can_change(self, obj, data): def can_change(self, obj, data):
return self.user.is_superuser and obj.status == 'new' return self.user.is_superuser and obj.status == 'new'
@@ -537,10 +667,16 @@ class JobHostSummaryAccess(BaseAccess):
model = JobHostSummary model = JobHostSummary
def get_queryset(self):
return self.model.objects.distinct() # FIXME
class JobEventAccess(BaseAccess): class JobEventAccess(BaseAccess):
model = JobEvent model = JobEvent
def get_queryset(self):
return self.model.objects.distinct() # FIXME
register_access(User, UserAccess) register_access(User, UserAccess)
register_access(Organization, OrganizationAccess) register_access(Organization, OrganizationAccess)
register_access(Inventory, InventoryAccess) register_access(Inventory, InventoryAccess)

View File

@@ -1,3 +1,6 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Django REST Framework # Django REST Framework
from rest_framework import authentication from rest_framework import authentication
from rest_framework import exceptions from rest_framework import exceptions

View File

@@ -6,66 +6,90 @@ import json
# Django # Django
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from django.utils.timezone import now from django.utils.timezone import now
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import mixins
from rest_framework import generics from rest_framework import generics
from rest_framework import permissions
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# AWX # AWX
from awx.main.models import * 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 # FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS
class BaseList(generics.ListCreateAPIView): class ListAPIView(generics.ListAPIView):
# Base class for a read-only list view.
permission_classes = (CustomRbac,)
# Subclasses should define: # Subclasses should define:
# model = ModelClass # model = ModelClass
# serializer_class = SerializerClass # serializer_class = SerializerClass
def post(self, request, *args, **kwargs): def get_queryset(self):
# FIXME: Should inherit from generics.ListAPIView if not postable. return self.request.user.get_queryset(self.model)
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)
# 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): class SubListAPIView(ListAPIView):
# Base class for a read-only sublist view.
''' used for subcollections with an overriden post '''
# Subclasses should define at least: # Subclasses should define at least:
# model = ModelClass # model = ModelClass
# serializer_class = SerializerClass # serializer_class = SerializerClass
# parent_model = ModelClass # parent_model = ModelClass
# relationship = 'rel_name_from_parent_to_model' # relationship = 'rel_name_from_parent_to_model'
# And optionally: # And optionally (user must have given access permission on parent object
# postable = True/False # 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' # inject_primary_key_on_post_as = 'field_on_model_referring_to_parent'
# severable = True/False # 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) inject_primary_key = getattr(self, 'inject_primary_key_on_post_as', None)
created = False
ser = None
postable = getattr(self.__class__, 'postable', False) if inject_primary_key is None:
if not postable: # view didn't specify a way to get the pk from the URL, so not even trying
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) 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 # Make a copy of the data provided (since it's readonly) in order to
# inject additional data. # inject additional data.
@@ -73,157 +97,100 @@ class BaseSubList(BaseList):
data = request.DATA.dict() data = request.DATA.dict()
else: else:
data = request.DATA 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: # attempt to deserialize the object
subs = self.__class__.model.objects.filter(pk=sub_id) serializer = self.serializer_class(data=data)
else: if not serializer.is_valid():
if 'disassociate' in data: return Response(serializer.errors,
raise PermissionDenied() # ID is required to disassociate status=status.HTTP_400_BAD_REQUEST)
else:
# this is a little tricky and a little manual # Verify we have permission to add the object as given.
# the object ID was not specified, so it probably doesn't exist in the DB yet. if not request.user.can_access(self.model, 'add', serializer.init_data):
# we want to see if we can create it. The URL may choose to inject it's primary key into the object raise PermissionDenied()
# because we are posting to a subcollection. Use all the normal access control mechanisms.
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 def attach(self, request, *args, **kwargs):
data[inject_primary_key] = kwargs['pk'] 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 # Create the sub object if an ID is not provided.
ser = self.__class__.serializer_class(data=data) if not sub_id:
if not ser.is_valid(): response = self.create(request, *args, **kwargs)
return Response(status=status.HTTP_400_BAD_REQUEST, data=ser.errors) 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): # Retrive the sub object (whether created or by ID).
raise PermissionDenied() 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 # Attach the object to the collection.
obj = ser.save() if sub not in relationship.all():
ser = self.__class__.serializer_class(obj) relationship.add(sub)
# 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()
if created: if created:
return Response(status=status.HTTP_201_CREATED, data=ser.data) return Response(data, status=status.HTTP_201_CREATED)
else: else:
return Response(status=status.HTTP_204_NO_CONTENT) 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): def pre_save(self, obj):
if type(obj) not in [ User ]: if type(obj) not in [ User ]:
@@ -237,23 +204,18 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView):
raise Http404() raise Http404()
if getattr(obj, 'is_active', True) == False: if getattr(obj, 'is_active', True) == False:
raise Http404() raise Http404()
#if not request.user.is_superuser and not self.delete_permissions_check(request, obj): if not request.user.can_access(self.model, 'delete', obj):
if not check_user_access(request.user, self.model, 'delete', obj):
raise PermissionDenied() raise PermissionDenied()
if isinstance(obj, PrimordialModel): if hasattr(obj, 'mark_inactive'):
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: else:
raise Exception("InternalError: destroy() not implemented yet for %s" % obj) raise Exception("InternalError: destroy() not implemented yet for %s" % obj)
return HttpResponse(status=204) return HttpResponse(status=204)
def put(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
self.put_filter(request, *args, **kwargs) self.update_filter(request, *args, **kwargs)
return super(BaseDetail, self).put(request, *args, **kwargs) return super(RetrieveUpdateDestroyAPIView, self).update(request, *args, **kwargs)
def put_filter(self, request, *args, **kwargs): def update_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 ''' ''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
pass pass

View File

@@ -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. Compability library for support of both Django 1.4.x and Django 1.5.x.
''' '''

View File

@@ -1,10 +1,10 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Django REST Framework
from rest_framework.filters import BaseFilterBackend 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): def filter_queryset(self, request, queryset, view):

View File

@@ -33,6 +33,10 @@ from djcelery.models import TaskMeta
# Django-REST-Framework # Django-REST-Framework
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
__all__ = ['Organization', 'Team', 'Project', 'Credential', 'Inventory',
'Host', 'Group', 'Permission', 'JobTemplate', 'Job',
'JobHostSummary', 'JobEvent']
# TODO: reporting model TBD # TODO: reporting model TBD
PERM_INVENTORY_ADMIN = 'admin' PERM_INVENTORY_ADMIN = 'admin'
@@ -1213,6 +1217,21 @@ class JobEvent(models.Model):
# TODO: reporting (MPD) # 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) @receiver(post_save, sender=User)
def create_auth_token_for_user(sender, **kwargs): def create_auth_token_for_user(sender, **kwargs):
instance = kwargs.get('instance', None) instance = kwargs.get('instance', None)

View File

@@ -1,26 +1,38 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import logging import logging
# Django
from django.http import Http404 from django.http import Http404
# Django REST Framework
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import permissions from rest_framework import permissions
# AWX
from awx.main.access import * from awx.main.access import *
from awx.main.models 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): def check_options_permissions(self, request, view, obj=None):
return self._check_get_permissions(request, view, obj) return self.check_get_permissions(request, view, obj)
def _check_head_permissions(self, request, view, obj=None): def check_head_permissions(self, request, view, obj=None):
return self._check_get_permissions(request, view, obj) 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'): if hasattr(view, 'parent_model'):
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk']) parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
if not check_user_access(request.user, view.parent_model, 'read', if not check_user_access(request.user, view.parent_model, 'read',
@@ -30,7 +42,7 @@ class CustomRbac(permissions.BasePermission):
return True return True
return check_user_access(request.user, view.model, 'read', obj) 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'): if hasattr(view, 'parent_model'):
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk']) parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
return True return True
@@ -39,7 +51,7 @@ class CustomRbac(permissions.BasePermission):
return True return True
return check_user_access(request.user, view.model, 'add', request.DATA) 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: if not obj:
return True # FIXME: For some reason this needs to return True return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None? # 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, return check_user_access(request.user, view.model, 'change', obj,
request.DATA) 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: if not obj:
return True # FIXME: For some reason this needs to return True return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None? # because it is first called with obj=None?
return check_user_access(request.user, view.model, 'delete', obj) 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. # Check that obj (if given) is active, otherwise raise a 404.
active = getattr(obj, 'active', getattr(obj, 'is_active', True)) 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 # Check permissions for the given view and object, based on the request
# method used. # method used.
check_method = getattr(self, '_check_%s_permissions' % \ check_method = getattr(self, 'check_%s_permissions' % \
request.method.lower(), None) request.method.lower(), None)
result = check_method and check_method(request, view, obj) result = check_method and check_method(request, view, obj)
if not result: if not result:
raise PermissionDenied() raise PermissionDenied()
return result 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): def has_permission(self, request, view, obj=None):
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)', logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
request.user, request.method, request.DATA, request.user, request.method, request.DATA,
view.__class__.__name__, obj) view.__class__.__name__, obj)
try: try:
response = self._check_permissions(request, view, obj) response = self.check_permissions(request, view, obj)
except Exception, e: except Exception, e:
logger.debug('has_permission raised %r', e, exc_info=True) logger.debug('has_permission raised %r', e, exc_info=True)
raise raise
@@ -122,7 +122,7 @@ class CustomRbac(permissions.BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return self.has_permission(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 Permission check used by job template callback view for requests from
empheral hosts. empheral hosts.
@@ -149,7 +149,7 @@ class JobTemplateCallbackPermission(CustomRbac):
else: else:
return True return True
class JobTaskPermission(CustomRbac): class JobTaskPermission(ModelAccessPermission):
''' '''
Permission checks used for API callbacks from running a task. Permission checks used for API callbacks from running a task.
''' '''

View File

@@ -1,19 +1,26 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import cStringIO import cStringIO
import json import json
import logging import logging
import os import os
import select
import subprocess import subprocess
import tempfile import tempfile
import time
import traceback import traceback
from celery import Task
from django.conf import settings # Pexpect
import 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'] __all__ = ['RunJob']
@@ -39,7 +46,6 @@ class RunJob(Task):
if field == 'status': if field == 'status':
update_fields.append('failed') update_fields.append('failed')
job.save(update_fields=update_fields) job.save(update_fields=update_fields)
# FIXME: Commit transaction?
return job return job
def get_path_to(self, *args): def get_path_to(self, *args):
@@ -165,6 +171,7 @@ class RunJob(Task):
r'Bad passphrase, try again for .*:', r'Bad passphrase, try again for .*:',
r'sudo password.*:', r'sudo password.*:',
r'SSH password:', r'SSH password:',
r'Password:',
pexpect.TIMEOUT, pexpect.TIMEOUT,
pexpect.EOF, pexpect.EOF,
] ]
@@ -175,7 +182,7 @@ class RunJob(Task):
child.sendline('') child.sendline('')
elif result_id == 2: elif result_id == 2:
child.sendline(passwords.get('sudo_password', '')) child.sendline(passwords.get('sudo_password', ''))
elif result_id == 3: elif result_id in (3, 4):
child.sendline(passwords.get('ssh_password', '')) child.sendline(passwords.get('ssh_password', ''))
job_updates = {} job_updates = {}
if logfile_pos != logfile.tell(): if logfile_pos != logfile.tell():

View File

@@ -187,9 +187,7 @@ class CleanupDeletedTest(BaseCommandTest):
self.assertEqual(counts_before, counts_after) self.assertEqual(counts_before, counts_after)
# "Delete some users". # "Delete some users".
for user in User.objects.all(): for user in User.objects.all():
user.username = "_deleted_%s_%s" % (now().isoformat(), user.username) user.mark_inactive()
user.is_active = False
user.save()
# With days=1, no users will be deleted. # With days=1, no users will be deleted.
counts_before = self.get_user_counts() counts_before = self.get_user_counts()
self.assertTrue(counts_before[1]) self.assertTrue(counts_before[1])

File diff suppressed because it is too large Load Diff

View File

@@ -147,8 +147,11 @@ REST_FRAMEWORK = {
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
), ),
'DEFAULT_PERMISSION_CLASSES': (
'awx.main.permissions.ModelAccessPermission',
),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
'awx.main.custom_filters.CustomFilterBackend', 'awx.main.filters.DefaultFilterBackend',
), ),
'DEFAULT_PARSER_CLASSES': ( 'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser', 'rest_framework.parsers.JSONParser',
@@ -323,7 +326,7 @@ LOGGING = {
'handlers': ['console', 'file', 'syslog'], 'handlers': ['console', 'file', 'syslog'],
'level': 'DEBUG', 'level': 'DEBUG',
}, },
'awx.main.rbac': { 'awx.main.permissions': {
'handlers': ['null'], 'handlers': ['null'],
# Comment the line below to show lots of permissions logging. # Comment the line below to show lots of permissions logging.
'propagate': False, 'propagate': False,