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.
# 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)

View File

@ -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

View File

@ -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

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.
'''

View File

@ -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):

View File

@ -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)

View File

@ -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.
'''

View File

@ -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():

View File

@ -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])

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.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,