Permissions-related updates and fixes, more tests, better handling of any user input that would previously generate a server error.

This commit is contained in:
Chris Church
2013-07-22 21:20:28 -04:00
parent 5ef8a48600
commit bc49627203
15 changed files with 558 additions and 326 deletions

View File

@@ -104,6 +104,10 @@ LOGGING['handlers']['syslog'] = {
# 'formatter': 'simple', # 'formatter': 'simple',
#} #}
# Enable the following lines to turn on lots of permissions-related logging.
#LOGGING['loggers']['awx.main.access']['propagate'] = True
#LOGGING['loggers']['awx.main.permissions']['propagate'] = True
# Define additional environment variables to be passed to subprocess started by # Define additional environment variables to be passed to subprocess started by
# the celery task. # the celery task.
#AWX_TASK_ENV['FOO'] = 'BAR' #AWX_TASK_ENV['FOO'] = 'BAR'

View File

@@ -13,6 +13,7 @@ from django.contrib.auth.models import User
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
# AWX # AWX
from awx.main.utils import *
from awx.main.models import * from awx.main.models import *
from awx.main.licenses import LicenseReader from awx.main.licenses import LicenseReader
@@ -140,15 +141,25 @@ class BaseAccess(object):
return self.can_change(obj, None) return self.can_change(obj, None)
class UserAccess(BaseAccess): class UserAccess(BaseAccess):
'''
I can see user records when:
- I'm a superuser.
- I'm that user.
- I'm their org admin.
- I'm on a team with that user.
I can change some fields for a user (mainly password) when I am that user.
I can change all fields for a user (admin access) or delete when:
- I'm a superuser.
- I'm their org admin.
'''
model = User model = User
def get_queryset(self): def get_queryset(self):
# I can see user records when I'm a superuser, I'm that user, I'm qs = self.model.objects.filter(is_active=True).distinct()
# their org admin, or I'm on a team with that user.
if self.user.is_superuser: if self.user.is_superuser:
return self.model.objects.all() return qs
return self.model.objects.filter(is_active=True).filter( return qs.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.user.teams.all()) Q(teams__in=self.user.teams.all())
@@ -167,31 +178,40 @@ class UserAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
# A user can be changed if they are themselves, or by org admins or # A user can be changed if they are themselves, or by org admins or
# superusers. # superusers. Change permission implies changing only certain fields
# that a user should be able to edit for themselves.
return bool(self.user == obj or self.can_admin(obj, data))
def can_admin(self, obj, data):
# Admin implies changing all user fields.
if self.user.is_superuser: if self.user.is_superuser:
return True return True
if self.user == obj:
return 'partial'
return bool(obj.organizations.filter(admins__in=[self.user]).count()) return bool(obj.organizations.filter(admins__in=[self.user]).count())
def can_delete(self, obj): def can_delete(self, obj):
if obj == self.user: if obj == self.user:
# cannot delete yourself # cannot delete yourself
return False return False
super_users = User.objects.filter(is_superuser=True) super_users = User.objects.filter(is_active=True, is_superuser=True)
if obj.is_superuser and super_users.count() == 1: if obj.is_superuser and super_users.count() == 1:
# cannot delete the last superuser # cannot delete the last active superuser
return False return False
return bool(self.user.is_superuser or return bool(self.user.is_superuser or
obj.organizations.filter(admins__in=[self.user]).count()) obj.organizations.filter(admins__in=[self.user]).count())
class OrganizationAccess(BaseAccess): class OrganizationAccess(BaseAccess):
'''
I can see organizations when:
- I am a superuser.
- I am an admin or user in that organization.
I can change or delete organizations when:
- I am a superuser.
- I'm an admin of that organization.
'''
model = Organization model = Organization
def get_queryset(self): 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() qs = self.model.objects.distinct()
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
@@ -203,146 +223,103 @@ class OrganizationAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
return bool(self.user.is_superuser or return bool(self.user.is_superuser or
obj.created_by == self.user or
self.user in obj.admins.all()) self.user in obj.admins.all())
def can_delete(self, obj): def can_delete(self, obj):
return self.can_change(obj, None) return self.can_change(obj, None)
class InventoryAccess(BaseAccess): class InventoryAccess(BaseAccess):
'''
I can see inventory when:
- I'm a superuser.
- I'm an org admin of the inventory's org.
- I have read, write or admin permissions on it.
I can change inventory when:
- I'm a superuser.
- I'm an org admin of the inventory's org.
- I have write or admin permissions on it.
I can delete inventory when:
- I'm a superuser.
- I'm an org admin of the inventory's org.
- I have admin permissions on it.
'''
model = Inventory model = Inventory
def get_queryset(self): def get_queryset(self, allowed=None):
# I can see inventory when I'm a superuser, an org admin of the allowed = allowed or PERMISSION_TYPES_ALLOWING_INVENTORY_READ
# inventory, or I have permissions on it. qs = Inventory.objects.filter(active=True).distinct()
base = Inventory.objects.distinct()
if self.user.is_superuser: if self.user.is_superuser:
return base.all() return qs
admin_of = base.filter(organization__admins__in = [ self.user ]).distinct() admin_of = qs.filter(organization__admins__in=[self.user]).distinct()
has_user_perms = base.filter( has_user_perms = qs.filter(
permissions__user__in = [ self.user ], permissions__user__in=[self.user],
permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, permissions__permission_type__in=allowed,
).distinct() ).distinct()
has_team_perms = base.filter( has_team_perms = qs.filter(
permissions__team__in = self.user.teams.all(), permissions__team__users__in=[self.user],
permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ, permissions__permission_type__in=allowed,
).distinct() ).distinct()
return admin_of | has_user_perms | has_team_perms 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: return bool(obj and self.get_queryset(allowed).filter(pk=obj.pk).count())
return True
by_org_admin = obj.organization.admins.filter(pk = self.user.pk).count()
by_team_permission = obj.permissions.filter(
team__in = self.user.teams.all(),
permission_type__in = allowed
).count()
by_user_permission = obj.permissions.filter(
user = self.user,
permission_type__in = allowed
).count()
result = (by_org_admin + by_team_permission + by_user_permission)
return result > 0
def _has_any_inventory_permission_types(self, allowed):
'''
rather than checking for a permission on a specific inventory, return whether we have
permissions on any inventory. This is primarily used to decide if the user can create
host or group objects
'''
if self.user.is_superuser:
return True
by_org_admin = self.user.organizations.filter(
admins__in = [ self.user ]
).count()
by_team_permission = Permission.objects.filter(
team__in = self.user.teams.all(),
permission_type__in = allowed
).count()
by_user_permission = self.user.permissions.filter(
permission_type__in = allowed
).count()
result = (by_org_admin + by_team_permission + by_user_permission)
return result > 0
def can_read(self, obj): def can_read(self, obj):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_READ) return self.has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_READ)
def can_add(self, data): def can_add(self, data):
if not 'organization' in data: # If no data is specified, just checking for generic add permission?
return True if not data:
return bool(self.user.is_superuser or self.user.admin_of_organizations.count())
# Otherwise, verify that the user has access to change the parent
# organization of this inventory.
if self.user.is_superuser: if self.user.is_superuser:
return True return True
if not self.user.is_superuser: else:
org = Organization.objects.get(pk=data['organization']) org = get_object_or_400(Organization, pk=data.get('organization', None))
if self.user in org.admins.all(): if self.user.can_access(Organization, 'change', org, None):
return True return True
return False return False
def can_change(self, obj, data): def can_change(self, obj, data):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) # Verify that the user has access to the given organization.
if data and 'organization' in data and not self.can_add(data):
return False
return self.has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
def can_admin(self, obj, data): def can_admin(self, obj, data):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN) # Verify that the user has access to the given organization.
if data and 'organization' in data and not self.can_add(data):
return False
return self.has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN)
def can_delete(self, obj): def can_delete(self, obj):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN) return self.can_admin(obj, None)
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
''' 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 self.user.can_access(type(sub_obj), 'read', sub_obj):
return False
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
def can_unattach(self, obj, sub_obj, relationship):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
class HostAccess(BaseAccess): class HostAccess(BaseAccess):
'''
I can see hosts whenever I can see their inventory.
I can change or delete hosts whenver I can change their inventory.
'''
model = Host model = Host
def get_queryset(self): def get_queryset(self):
''' qs = self.model.objects.filter(active=True).distinct()
I can see hosts when: inventories_qs = self.user.get_queryset(Inventory)
I'm a superuser, return qs.filter(inventory__in=inventories_qs)
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 self.user.can_access(Inventory, 'read', obj.inventory) return obj and self.user.can_access(Inventory, 'read', obj.inventory)
def can_add(self, data): def can_add(self, data):
if not data or not 'inventory' in data:
if not 'inventory' in data:
return False return False
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 = self.user.can_access(Inventory, 'change', inventory, None) inventory = get_object_or_400(Inventory, pk=data.get('inventory', None))
if not permissions_ok: if not self.user.can_access(Inventory, 'change', inventory, None):
return False return False
# Check to see if we have enough licenses # Check to see if we have enough licenses
@@ -361,57 +338,46 @@ class HostAccess(BaseAccess):
raise PermissionDenied("license range of %s instances has been exceed" % instances) raise PermissionDenied("license range of %s instances has been exceed" % instances)
def can_change(self, obj, data): def can_change(self, obj, data):
# Prevent moving a host to a different inventory.
if obj and data and obj.inventory.pk != data.get('inventory', None):
raise PermissionDenied('Unable to change inventory on a host')
# 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 self.user.can_access(Inventory, 'change', obj.inventory, None) return obj and self.user.can_access(Inventory, 'change', obj.inventory, None)
class GroupAccess(BaseAccess): class GroupAccess(BaseAccess):
'''
I can see groups whenever I can see their inventory.
I can change or delete groups whenever I can change their inventory.
'''
model = Group model = Group
def get_queryset(self): def get_queryset(self):
''' qs = self.model.objects.filter(active=True).distinct()
I can see groups when: inventories_qs = self.user.get_queryset(Inventory)
I'm a superuser, return qs.filter(inventory__in=inventories_qs)
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 self.user.can_access(Inventory, 'read', obj.inventory) return obj and 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 data or not 'inventory' in data:
return False return False
inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on inventory. # Checks for admin or change permission on inventory.
inventory = get_object_or_400(Inventory, pk=data.get('inventory', None))
return self.user.can_access(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 self.user.can_access(Inventory, 'change', obj.inventory, None) return obj and self.user.can_access(Inventory, 'change', obj.inventory, None)
def can_attach(self, obj, sub_obj, relationship, data, def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False): skip_sub_obj_read_check=False):
if not self.can_change(obj, None): if not super(GroupAccess, self).can_attach(obj, sub_obj, relationship,
data, skip_sub_obj_read_check):
return False 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. # Prevent group from being assigned as its own (grand)child.
if type(obj) == type(sub_obj): if type(obj) == type(sub_obj):
@@ -425,22 +391,33 @@ class GroupAccess(BaseAccess):
return True return True
class CredentialAccess(BaseAccess): class CredentialAccess(BaseAccess):
'''
I can see credentials when:
- I'm a superuser.
- It's a user credential and it's my credential.
- It's a user credential and I'm an admin of an organization where that
user is a member of admin of the organization.
- It's a team credential and I'm an admin of the team's organization.
- It's a team credential and I'm a member of the team.
'''
model = Credential model = Credential
def get_queryset(self): def get_queryset(self):
# I can see credentials when: qs = self.model.objects.filter(active=True).distinct()
# - 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: if self.user.is_superuser:
return qs return qs
return qs.filter(Q(user=self.user)) orgs_as_admin = self.user.admin_of_organizations.all()
return qs.filter(
Q(user=self.user) |
Q(user__organizations__in=orgs_as_admin) |
Q(user__admin_of_organizations__in=orgs_as_admin) |
Q(team__organization__in=orgs_as_admin) |
Q(team__users__in=[self.user])
)
def can_read(self, obj): def can_read(self, obj):
return self.can_change(obj, None) return obj and self.can_change(obj, None)
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -476,7 +453,17 @@ class TeamAccess(BaseAccess):
model = Team model = Team
def get_queryset(self): def get_queryset(self):
return self.model.objects.distinct() # FIXME # I can see a team when:
# - I'm a superuser.
# - I'm an admin of the team's organization.
# - I'm a member of that team.
qs = self.model.objects.filter(active=True).distinct()
if self.user.is_superuser:
return qs
return qs.filter(
Q(organization__admins__in=[self.user]) |
Q(users__in=[self.user])
)
def can_add(self, data): def can_add(self, data):
if self.user.is_superuser: if self.user.is_superuser:
@@ -587,13 +574,16 @@ class JobTemplateAccess(BaseAccess):
project's orgs, or if I'm in a team on the project. This does not mean project's orgs, or if I'm in a team on the project. This does not mean
I would be able to launch a job from the template or edit the template. I would be able to launch a job from the template or edit the template.
''' '''
# FIXME: Don't think this is quite right...
qs = self.model.objects.all() qs = self.model.objects.all()
if self.user.is_superuser: if self.user.is_superuser:
return qs.all() return qs.all()
return qs.filter(active=True).filter( qs = qs.filter(active=True).filter(
Q(project__organizations__admins__in=[self.user]) | Q(project__organizations__admins__in=[self.user]) |
Q(project__teams__users__in=[self.user]) Q(project__teams__users__in=[self.user])
).distinct() ).distinct()
#print qs.values_list('name', flat=True)
return qs
def can_read(self, obj): def can_read(self, obj):
# you can only see the job templates that you have permission to launch. # you can only see the job templates that you have permission to launch.

View File

@@ -31,9 +31,23 @@ class ListAPIView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
return self.request.user.get_queryset(self.model) return self.request.user.get_queryset(self.model)
def get_description_vars(self):
return {
'model_verbose_name': unicode(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
}
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
return s % self.get_description_vars()
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
# Base class for a list view that allows creating new objects. # Base class for a list view that allows creating new objects.
pass
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
return '\n\n'.join([s, s2]) % self.get_description_vars()
class SubListAPIView(ListAPIView): class SubListAPIView(ListAPIView):
# Base class for a read-only sublist view. # Base class for a read-only sublist view.
@@ -47,6 +61,18 @@ class SubListAPIView(ListAPIView):
# to view sublist): # to view sublist):
# parent_access = 'admin' # parent_access = 'admin'
def get_description_vars(self):
d = super(SubListAPIView, self).get_description_vars()
d.update({
'parent_model_verbose_name': unicode(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': unicode(self.parent_model._meta.verbose_name_plural),
})
return d
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
return s % self.get_description_vars()
def get_parent_object(self): def get_parent_object(self):
parent_filter = { parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None), self.lookup_field: self.kwargs.get(self.lookup_field, None),
@@ -70,13 +96,23 @@ class SubListAPIView(ListAPIView):
sublist_qs = getattr(parent, self.relationship).distinct() sublist_qs = getattr(parent, self.relationship).distinct()
return qs & sublist_qs return qs & sublist_qs
class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView): class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
# Base class for a sublist view that allows for creating subobjects and # Base class for a sublist view that allows for creating subobjects and
# attaching/detaching them from the parent. # attaching/detaching them from the parent.
# In addition to SubListAPIView properties, subclasses may define: # In addition to SubListAPIView properties, subclasses may define:
# inject_primary_key_on_post_as = 'field_on_model_referring_to_parent' # parent_key = 'field_on_model_referring_to_parent'
# severable = True/False
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
if getattr(self, 'parent_key', None):
s3 = 'Use a POST request with an `id` field and `disassociate` set to delete the associated %(model_verbose_name)s.'
s4 = ''
else:
s3 = 'Use a POST request with only an `id` field to associate an existing %(model_verbose_name)s with this %(parent_model_verbose_name)s.'
s4 = 'Use a POST request with an `id` field and `disassociate` set to remove the %(model_verbose_name)s from this %(parent_model_verbose_name)s without deleting the %(model_verbose_name)s.'
return '\n\n'.join(filter(None, [s, s2, s3, s4])) % self.get_description_vars()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
# If the object ID was not specified, it probably doesn't exist in the # If the object ID was not specified, it probably doesn't exist in the
@@ -84,13 +120,6 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
# inject it's primary key into the object because we are posting to a # inject it's primary key into the object because we are posting to a
# subcollection. Use all the normal access control mechanisms. # subcollection. Use all the normal access control mechanisms.
inject_primary_key = getattr(self, 'inject_primary_key_on_post_as', None)
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 # Make a copy of the data provided (since it's readonly) in order to
# inject additional data. # inject additional data.
if hasattr(request.DATA, 'dict'): if hasattr(request.DATA, 'dict'):
@@ -98,8 +127,10 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
else: else:
data = request.DATA data = request.DATA
# add the key to the post data using the pk from the URL # add the parent key to the post data using the pk from the URL
data[inject_primary_key] = kwargs['pk'] parent_key = getattr(self, 'parent_key', None)
if parent_key:
data[parent_key] = self.kwargs['pk']
# attempt to deserialize the object # attempt to deserialize the object
serializer = self.serializer_class(data=data) serializer = self.serializer_class(data=data)
@@ -160,7 +191,7 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
parent = self.get_parent_object() parent = self.get_parent_object()
severable = getattr(self, 'severable', True) parent_key = getattr(self, 'parent_key', None)
relationship = getattr(parent, self.relationship) relationship = getattr(parent, self.relationship)
try: try:
@@ -172,11 +203,12 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship): if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship):
raise PermissionDenied() raise PermissionDenied()
if severable: if parent_key:
relationship.remove(sub) # sub object has a ForeignKey to the parent, so we can't remove it
else: # from the set, only mark it as inactive.
# resource is just a ForeignKey, can't remove it from the set, just set it inactive
sub.mark_inactive() sub.mark_inactive()
else:
relationship.remove(sub)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@@ -186,7 +218,6 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
else: else:
return self.attach(request, *args, **kwargs) return self.attach(request, *args, **kwargs)
class RetrieveAPIView(generics.RetrieveAPIView): class RetrieveAPIView(generics.RetrieveAPIView):
pass pass

View File

@@ -1,16 +1,17 @@
# Copyright (c) 2013 AnsibleWorks, Inc. # Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved. # All Rights Reserved.
# Django
from django.core.exceptions import FieldError
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import ParseError
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
class DefaultFilterBackend(BaseFilterBackend): class DefaultFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
terms = {}
order_by = None
# Filtering by is_active/active that was previously in BaseList. # Filtering by is_active/active that was previously in BaseList.
qs = queryset qs = queryset
for field in queryset.model._meta.fields: for field in queryset.model._meta.fields:
@@ -19,25 +20,34 @@ class DefaultFilterBackend(BaseFilterBackend):
elif field.name == 'active': elif field.name == 'active':
qs = qs.filter(active=True) qs = qs.filter(active=True)
for key, value in request.QUERY_PARAMS.items(): # Apply filters and ordering specified via QUERY_PARAMS.
try:
if key in [ 'page', 'page_size', 'format' ]: filters = {}
continue order_by = None
if key in ('order', 'order_by'): for key, value in request.QUERY_PARAMS.items():
order_by = value
continue
key2 = key if key in ('page', 'page_size', 'format'):
if key2.endswith("__int"): continue
key2 = key.replace("__int","")
value = int(value)
terms[key2] = value if key in ('order', 'order_by'):
order_by = value
continue
qs = qs.filter(**terms) if key.endswith('__int'):
key = key.replace('__int', '')
value = int(value)
if order_by: filters[key] = value
qs = qs.order_by(order_by)
qs = qs.filter(**filters)
if order_by:
qs = qs.order_by(order_by)
except (FieldError, ValueError), e:
# Handle invalid field names or values and return a 400.
raise ParseError(*e.args)
return qs return qs

View File

@@ -898,7 +898,7 @@ class JobHostSummary(models.Model):
class Meta: class Meta:
unique_together = [('job', 'host')] unique_together = [('job', 'host')]
verbose_name_plural = _('Job Host Summaries') verbose_name_plural = _('job host summaries')
ordering = ('-pk',) ordering = ('-pk',)
job = models.ForeignKey( job = models.ForeignKey(

View File

@@ -124,13 +124,15 @@ class BaseTestMixin(object):
self.normal_password = 'normal' self.normal_password = 'normal'
self.other_username = 'other' self.other_username = 'other'
self.other_password = 'other' self.other_password = 'other'
self.nobody_username = 'nobody'
self.nobody_password = 'nobody'
self.super_django_user = self.make_user(self.super_username, self.super_password, super_user=True) self.super_django_user = self.make_user(self.super_username, self.super_password, super_user=True)
if not just_super_user: if not just_super_user:
self.normal_django_user = self.make_user(self.normal_username, self.normal_password, super_user=False) self.normal_django_user = self.make_user(self.normal_username, self.normal_password, super_user=False)
self.other_django_user = self.make_user(self.other_username, self.other_password, super_user=False) self.other_django_user = self.make_user(self.other_username, self.other_password, super_user=False)
self.nobody_django_user = self.make_user(self.nobody_username, self.nobody_password, super_user=False)
def get_super_credentials(self): def get_super_credentials(self):
return (self.super_username, self.super_password) return (self.super_username, self.super_password)
@@ -141,6 +143,10 @@ class BaseTestMixin(object):
def get_other_credentials(self): def get_other_credentials(self):
return (self.other_username, self.other_password) return (self.other_username, self.other_password)
def get_nobody_credentials(self):
# here is a user without any permissions...
return (self.nobody_username, self.nobody_password)
def get_invalid_credentials(self): def get_invalid_credentials(self):
return ('random', 'combination') return ('random', 'combination')
@@ -239,6 +245,39 @@ class BaseTestMixin(object):
data = self.get(collection_url, expect=200, auth=auth) data = self.get(collection_url, expect=200, auth=auth)
return [item['url'] for item in data['results']] return [item['url'] for item in data['results']]
def check_invalid_auth(self, url, data=None, methods=None):
'''
Check various methods of accessing the given URL with invalid
authentication credentials.
'''
data = data or {}
methods = methods or ('options', 'head', 'get')
for auth in [(None,), ('invalid', 'password')]:
with self.current_user(*auth):
for method in methods:
f = getattr(self, method)
if method in ('post', 'put', 'patch'):
f(url, data, expect=401)
else:
f(url, expect=401)
def check_get_list(self, url, user, qs, fields=None, expect=200):
'''
Check that the given list view URL returns results for the given user
that match the given queryset.
'''
with self.current_user(user):
self.options(url, expect=expect)
self.head(url, expect=expect)
response = self.get(url, expect=expect)
if expect != 200:
return
self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs)
if fields:
for obj in response['results']:
self.assertTrue(set(obj.keys()) <= set(fields))
class BaseTest(BaseTestMixin, django.test.TestCase): class BaseTest(BaseTestMixin, django.test.TestCase):
''' '''
Base class for unit tests. Base class for unit tests.

View File

@@ -33,15 +33,137 @@ class InventoryTest(BaseTest):
permission_type = 'read' permission_type = 'read'
) )
# and make one more user that won't be a part of any org, just for negative-access testing def test_get_inventory_list(self):
url = reverse('main:inventory_list')
qs = Inventory.objects.filter(active=True).distinct()
self.nobody_django_user = User.objects.create(username='nobody') # Check list view with invalid authentication.
self.nobody_django_user.set_password('nobody') self.check_invalid_auth(url)
self.nobody_django_user.save()
def get_nobody_credentials(self): # a super user can list all inventories
# here is a user without any permissions... self.check_get_list(url, self.super_django_user, qs)
return ('nobody', 'nobody')
# an org admin can list inventories but is filtered to what he adminsters
normal_qs = qs.filter(organization__admins__in=[self.normal_django_user])
self.check_get_list(url, self.normal_django_user, normal_qs)
# a user who is on a team who has a read permissions on an inventory can see filtered inventories
other_qs = qs.filter(permissions__user__in=[self.other_django_user])
self.check_get_list(url, self.other_django_user, other_qs)
# a regular user not part of anything cannot see any inventories
nobody_qs = qs.none()
self.check_get_list(url, self.nobody_django_user, nobody_qs)
def test_post_inventory_list(self):
url = reverse('main:inventory_list')
# Check post to list view with invalid authentication.
new_inv_0 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk)
self.check_invalid_auth(url, new_inv_0, methods=('post',))
# a super user can create inventory
new_inv_1 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk)
new_id = max(Inventory.objects.values_list('pk', flat=True)) + 1
with self.current_user(self.super_django_user):
data = self.post(url, data=new_inv_1, expect=201)
self.assertEquals(data['id'], new_id)
# an org admin of any org can create inventory, if it is one of his organizations
# the organization parameter is required!
new_inv_incomplete = dict(name='inventory-d', description='baz')
new_inv_not_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[2].pk)
new_inv_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[0].pk)
with self.current_user(self.normal_django_user):
data = self.post(url, data=new_inv_incomplete, expect=400)
data = self.post(url, data=new_inv_not_my_org, expect=403)
data = self.post(url, data=new_inv_my_org, expect=201)
# a regular user cannot create inventory
new_inv_denied = dict(name='inventory-e', description='glorp', organization=self.organizations[0].pk)
with self.current_user(self.other_django_user):
data = self.post(url, data=new_inv_denied, expect=403)
def test_get_inventory_detail(self):
url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
# Check detail view with invalid authentication.
self.check_invalid_auth(url_a)
self.check_invalid_auth(url_b)
# a super user can get inventory records
with self.current_user(self.super_django_user):
data = self.get(url_a, expect=200)
self.assertEquals(data['name'], 'inventory-a')
# an org admin can get inventory records for his orgs only
with self.current_user(self.normal_django_user):
data = self.get(url_a, expect=200)
self.assertEquals(data['name'], 'inventory-a')
data = self.get(url_b, expect=403)
# a user who is on a team who has read permissions on an inventory can see inventory records
with self.current_user(self.other_django_user):
data = self.get(url_a, expect=403)
data = self.get(url_b, expect=200)
self.assertEquals(data['name'], 'inventory-b')
# a regular user cannot read any inventory records
with self.current_user(self.nobody_django_user):
data = self.get(url_a, expect=403)
data = self.get(url_b, expect=403)
def test_put_inventory_detail(self):
url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
# Check put to detail view with invalid authentication.
self.check_invalid_auth(url_a, methods=('put',))
self.check_invalid_auth(url_b, methods=('put',))
# a super user can update inventory records
with self.current_user(self.super_django_user):
data = self.get(url_a, expect=200)
data['name'] = 'inventory-a-update1'
self.put(url_a, data, expect=200)
data = self.get(url_b, expect=200)
data['name'] = 'inventory-b-update1'
self.put(url_b, data, expect=200)
# an org admin can update inventory records for his orgs only.
with self.current_user(self.normal_django_user):
data = self.get(url_a, expect=200)
data['name'] = 'inventory-a-update2'
self.put(url_a, data, expect=200)
self.put(url_b, data, expect=403)
# a user who is on a team who has read permissions on an inventory can
# see inventory records, but not update.
with self.current_user(self.other_django_user):
data = self.get(url_b, expect=200)
data['name'] = 'inventory-b-update3'
self.put(url_b, data, expect=403)
# a regular user cannot update any inventory records
with self.current_user(self.nobody_django_user):
self.put(url_a, {}, expect=403)
self.put(url_b, {}, expect=403)
# a superuser can reassign an inventory to another organization.
with self.current_user(self.super_django_user):
data = self.get(url_b, expect=200)
self.assertEqual(data['organization'], self.organizations[1].pk)
data['organization'] = self.organizations[0].pk
self.put(url_b, data, expect=200)
# a normal user can't reassign an inventory to an organization where
# he isn't an admin.
with self.current_user(self.normal_django_user):
data = self.get(url_a, expect=200)
self.assertEqual(data['organization'], self.organizations[0].pk)
data['organization'] = self.organizations[1].pk
self.put(url_a, data, expect=403)
def test_main_line(self): def test_main_line(self):
@@ -53,57 +175,57 @@ class InventoryTest(BaseTest):
groups = reverse('main:group_list') groups = reverse('main:group_list')
# a super user can list inventories # a super user can list inventories
data = self.get(inventories, expect=200, auth=self.get_super_credentials()) #data = self.get(inventories, expect=200, auth=self.get_super_credentials())
self.assertEquals(data['count'], 2) #self.assertEquals(data['count'], 2)
# an org admin can list inventories but is filtered to what he adminsters # an org admin can list inventories but is filtered to what he adminsters
data = self.get(inventories, expect=200, auth=self.get_normal_credentials()) #data = self.get(inventories, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 1) #self.assertEquals(data['count'], 1)
# a user who is on a team who has a read permissions on an inventory can see filtered inventories # a user who is on a team who has a read permissions on an inventory can see filtered inventories
data = self.get(inventories, expect=200, auth=self.get_other_credentials()) #data = self.get(inventories, expect=200, auth=self.get_other_credentials())
self.assertEquals(data['count'], 1) #self.assertEquals(data['count'], 1)
# a regular user not part of anything cannot see any inventories # a regular user not part of anything cannot see any inventories
data = self.get(inventories, expect=200, auth=self.get_nobody_credentials()) #data = self.get(inventories, expect=200, auth=self.get_nobody_credentials())
self.assertEquals(data['count'], 0) #self.assertEquals(data['count'], 0)
# a super user can get inventory records # a super user can get inventory records
data = self.get(inventories_1, expect=200, auth=self.get_super_credentials()) #data = self.get(inventories_1, expect=200, auth=self.get_super_credentials())
self.assertEquals(data['name'], 'inventory-a') #self.assertEquals(data['name'], 'inventory-a')
# an org admin can get inventory records # an org admin can get inventory records
data = self.get(inventories_1, expect=200, auth=self.get_normal_credentials()) #data = self.get(inventories_1, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['name'], 'inventory-a') #self.assertEquals(data['name'], 'inventory-a')
# a user who is on a team who has read permissions on an inventory can see inventory records # a user who is on a team who has read permissions on an inventory can see inventory records
data = self.get(inventories_1, expect=403, auth=self.get_other_credentials()) #data = self.get(inventories_1, expect=403, auth=self.get_other_credentials())
data = self.get(inventories_2, expect=200, auth=self.get_other_credentials()) #data = self.get(inventories_2, expect=200, auth=self.get_other_credentials())
self.assertEquals(data['name'], 'inventory-b') #self.assertEquals(data['name'], 'inventory-b')
# a regular user cannot read any inventory records # a regular user cannot read any inventory records
data = self.get(inventories_1, expect=403, auth=self.get_nobody_credentials()) #data = self.get(inventories_1, expect=403, auth=self.get_nobody_credentials())
data = self.get(inventories_2, expect=403, auth=self.get_nobody_credentials()) #data = self.get(inventories_2, expect=403, auth=self.get_nobody_credentials())
# a super user can create inventory # a super user can create inventory
new_inv_1 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk) #new_inv_1 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk)
new_id = max(Inventory.objects.values_list('pk', flat=True)) + 1 #new_id = max(Inventory.objects.values_list('pk', flat=True)) + 1
data = self.post(inventories, data=new_inv_1, expect=201, auth=self.get_super_credentials()) #data = self.post(inventories, data=new_inv_1, expect=201, auth=self.get_super_credentials())
self.assertEquals(data['id'], new_id) #self.assertEquals(data['id'], new_id)
# an org admin of any org can create inventory, if it is one of his organizations # an org admin of any org can create inventory, if it is one of his organizations
# the organization parameter is required! # the organization parameter is required!
new_inv_incomplete = dict(name='inventory-d', description='baz') #new_inv_incomplete = dict(name='inventory-d', description='baz')
data = self.post(inventories, data=new_inv_incomplete, expect=400, auth=self.get_normal_credentials()) #data = self.post(inventories, data=new_inv_incomplete, expect=400, auth=self.get_normal_credentials())
new_inv_not_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[2].pk) #new_inv_not_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[2].pk)
data = self.post(inventories, data=new_inv_not_my_org, expect=403, auth=self.get_normal_credentials()) #data = self.post(inventories, data=new_inv_not_my_org, expect=403, auth=self.get_normal_credentials())
new_inv_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[0].pk) #new_inv_my_org = dict(name='inventory-d', description='baz', organization=self.organizations[0].pk)
data = self.post(inventories, data=new_inv_my_org, expect=201, auth=self.get_normal_credentials()) #data = self.post(inventories, data=new_inv_my_org, expect=201, auth=self.get_normal_credentials())
# a regular user cannot create inventory # a regular user cannot create inventory
new_inv_denied = dict(name='inventory-e', description='glorp', organization=self.organizations[0].pk) #new_inv_denied = dict(name='inventory-e', description='glorp', organization=self.organizations[0].pk)
data = self.post(inventories, data=new_inv_denied, expect=403, auth=self.get_other_credentials()) #data = self.post(inventories, data=new_inv_denied, expect=403, auth=self.get_other_credentials())
# a super user can add hosts (but inventory ID is required) # a super user can add hosts (but inventory ID is required)
inv = Inventory.objects.create( inv = Inventory.objects.create(

View File

@@ -12,7 +12,7 @@ import uuid
from django.contrib.auth.models import User as DjangoUser from django.contrib.auth.models import User as DjangoUser
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db.models import Q
import django.test import django.test
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
@@ -427,57 +427,49 @@ class BaseJobTestMixin(BaseTestMixin):
super(BaseJobTestMixin, self).setUp() super(BaseJobTestMixin, self).setUp()
self.populate() self.populate()
def _test_invalid_creds(self, url, data=None, methods=None):
data = data or {}
methods = methods or ('options', 'head', 'get')
for auth in [(None,), ('invalid', 'password')]:
with self.current_user(*auth):
for method in methods:
f = getattr(self, method)
if method in ('post', 'put', 'patch'):
f(url, data, expect=401)
else:
f(url, expect=401)
class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
JOB_TEMPLATE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created',
'name', 'description', 'job_type', 'inventory',
'project', 'playbook', 'credential', 'forks',
'limit', 'verbosity', 'extra_vars', 'job_tags',
'host_config_key',)
def test_get_job_template_list(self): def test_get_job_template_list(self):
url = reverse('main:job_template_list') url = reverse('main:job_template_list')
qs = JobTemplate.objects.distinct()
fields = self.JOB_TEMPLATE_FIELDS
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
# sue's credentials (superuser) == 200, full list # Sue's credentials (superuser) == 200, full list
with self.current_user(self.user_sue): self.check_get_list(url, self.user_sue, qs, fields)
self.options(url)
self.head(url)
response = self.get(url)
qs = JobTemplate.objects.all()
self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs)
# FIXME: Check individual job template result fields. # Alex's credentials (admin of all orgs) == 200, full list
self.check_get_list(url, self.user_alex, qs, fields)
# alex's credentials (admin of all orgs) == 200, full list # Bob's credentials (admin of eng, user of ops) == 200, all from
with self.current_user(self.user_alex):
self.options(url)
self.head(url)
response = self.get(url)
qs = JobTemplate.objects.all()
self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs)
# bob's credentials (admin of eng, user of ops) == 200, all from
# engineering and operations. # engineering and operations.
with self.current_user(self.user_bob): bob_qs = qs.filter(
self.options(url) Q(project__organizations__admins__in=[self.user_bob]) |
self.head(url) Q(project__teams__users__in=[self.user_bob]),
response = self.get(url)
qs = JobTemplate.objects.filter(
inventory__organization__in=[self.org_eng, self.org_ops],
) )
#self.check_pagination_and_size(response, qs.count()) self.check_get_list(url, self.user_bob, bob_qs, fields)
#self.check_list_ids(response, qs)
# Chuck's credentials (admin of eng) == 200, all from engineering.
chuck_qs = qs.filter(
Q(project__organizations__admins__in=[self.user_chuck]) |
Q(project__teams__users__in=[self.user_chuck]),
)
self.check_get_list(url, self.user_chuck, chuck_qs, fields)
# Doug's credentials (user of eng) == 200, none?.
doug_qs = qs.filter(
Q(project__organizations__admins__in=[self.user_doug]) |
Q(project__teams__users__in=[self.user_doug]),
)
self.check_get_list(url, self.user_doug, doug_qs, fields)
# FIXME: Check with other credentials. # FIXME: Check with other credentials.
@@ -492,7 +484,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
) )
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url, data, methods=('post',)) self.check_invalid_auth(url, data, methods=('post',))
# sue can always add job templates. # sue can always add job templates.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -541,7 +533,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_template_detail', args=(jt.pk,)) url = reverse('main:job_template_detail', args=(jt.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
# sue can read the job template detail. # sue can read the job template detail.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -562,7 +554,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_template_detail', args=(jt.pk,)) url = reverse('main:job_template_detail', args=(jt.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url, methods=('put',))# 'patch')) self.check_invalid_auth(url, methods=('put',))# 'patch'))
# sue can update the job template detail. # sue can update the job template detail.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -579,7 +571,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_template_jobs_list', args=(jt.pk,)) url = reverse('main:job_template_jobs_list', args=(jt.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
# sue can read the job template job list. # sue can read the job template job list.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -601,7 +593,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
) )
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url, data, methods=('post',)) self.check_invalid_auth(url, data, methods=('post',))
# sue can create a new job from the template. # sue can create a new job from the template.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -615,7 +607,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_list') url = reverse('main:job_list')
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
# sue's credentials (superuser) == 200, full list # sue's credentials (superuser) == 200, full list
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -641,7 +633,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
) )
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url, data, methods=('post',)) self.check_invalid_auth(url, data, methods=('post',))
# sue can create a new job without a template. # sue can create a new job without a template.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -663,7 +655,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_detail', args=(job.pk,)) url = reverse('main:job_detail', args=(job.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
# sue can read the job detail. # sue can read the job detail.
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -679,7 +671,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_detail', args=(job.pk,)) url = reverse('main:job_detail', args=(job.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url, methods=('put',))# 'patch')) self.check_invalid_auth(url, methods=('put',))# 'patch'))
# sue can update the job detail only if the job is new. # sue can update the job detail only if the job is new.
self.assertEqual(job.status, 'new') self.assertEqual(job.status, 'new')
@@ -780,8 +772,8 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
url = reverse('main:job_start', args=(job.pk,)) url = reverse('main:job_start', args=(job.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
self._test_invalid_creds(url, methods=('post',)) self.check_invalid_auth(url, methods=('post',))
# Sue can start a job (when passwords are already saved) as long as the # Sue can start a job (when passwords are already saved) as long as the
# status is new. Reverse list so "new" will be last. # status is new. Reverse list so "new" will be last.
@@ -867,8 +859,8 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
url = reverse('main:job_cancel', args=(job.pk,)) url = reverse('main:job_cancel', args=(job.pk,))
# Test with no auth and with invalid login. # Test with no auth and with invalid login.
self._test_invalid_creds(url) self.check_invalid_auth(url)
self._test_invalid_creds(url, methods=('post',)) self.check_invalid_auth(url, methods=('post',))
# sue can cancel the job, but only when it is pending or running. # sue can cancel the job, but only when it is pending or running.
for status in [x[0] for x in Job.STATUS_CHOICES]: for status in [x[0] for x in Job.STATUS_CHOICES]:

View File

@@ -84,14 +84,6 @@ class ProjectsTest(BaseTest):
self.team1.users.add(self.normal_django_user) self.team1.users.add(self.normal_django_user)
self.team2.users.add(self.other_django_user) self.team2.users.add(self.other_django_user)
self.nobody_django_user = User.objects.create(username='nobody')
self.nobody_django_user.set_password('nobody')
self.nobody_django_user.save()
def get_nobody_credentials(self):
# here is a user without any permissions...
return ('nobody', 'nobody')
def test_playbooks(self): def test_playbooks(self):
def write_test_file(project, name, content): def write_test_file(project, name, content):
full_path = os.path.join(project.get_project_path(), name) full_path = os.path.join(project.get_project_path(), name)
@@ -395,11 +387,13 @@ class ProjectsTest(BaseTest):
self.get(url, expect=401, auth=self.get_invalid_credentials()) self.get(url, expect=401, auth=self.get_invalid_credentials())
self.get(url, expect=403, auth=self.get_nobody_credentials()) self.get(url, expect=403, auth=self.get_nobody_credentials())
other.organizations.add(Organization.objects.get(pk=self.organizations[1].pk)) other.organizations.add(Organization.objects.get(pk=self.organizations[1].pk))
other.save() # Normal user can only see some teams that other user is a part of,
# since normal user is not an admin of that organization.
my_teams1 = self.get(url, expect=200, auth=self.get_normal_credentials()) my_teams1 = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEqual(my_teams1['count'], 1)
# Other user should be able to see all his own teams.
my_teams2 = self.get(url, expect=200, auth=self.get_other_credentials()) my_teams2 = self.get(url, expect=200, auth=self.get_other_credentials())
self.assertEqual(my_teams1['count'], 2) self.assertEqual(my_teams2['count'], 2)
self.assertEqual(my_teams1, my_teams2)
# ===================================================================== # =====================================================================
# USER PROJECTS # USER PROJECTS
@@ -511,14 +505,14 @@ class ProjectsTest(BaseTest):
team_url = reverse('main:team_credentials_list', args=(cred_put_t['team'],)) team_url = reverse('main:team_credentials_list', args=(cred_put_t['team'],))
self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials()) self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials())
# can remove credentials from a user (via disassociate) # can remove credentials from a user (via disassociate) - this will delete the credential.
cred_put_u['disassociate'] = 1 cred_put_u['disassociate'] = 1
url = cred_put_u['url'] url = cred_put_u['url']
user_url = reverse('main:user_credentials_list', args=(cred_put_u['user'],)) user_url = reverse('main:user_credentials_list', args=(cred_put_u['user'],))
self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials()) self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials())
# can delete a credential directly -- probably won't be used too often # can delete a credential directly -- probably won't be used too often
data = self.delete(url, expect=204, auth=self.get_other_credentials()) #data = self.delete(url, expect=204, auth=self.get_other_credentials())
data = self.delete(url, expect=404, auth=self.get_other_credentials()) data = self.delete(url, expect=404, auth=self.get_other_credentials())
# ===================================================================== # =====================================================================

View File

@@ -142,7 +142,7 @@ class UsersTest(BaseTest):
def test_user_list_filtered(self): def test_user_list_filtered(self):
url = reverse('main:user_list') url = reverse('main:user_list')
data3 = self.get(url, expect=200, auth=self.get_super_credentials()) data3 = self.get(url, expect=200, auth=self.get_super_credentials())
self.assertEquals(data3['count'], 3) self.assertEquals(data3['count'], 4)
data2 = self.get(url, expect=200, auth=self.get_normal_credentials()) data2 = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data2['count'], 2) self.assertEquals(data2['count'], 2)
data1 = self.get(url, expect=200, auth=self.get_other_credentials()) data1 = self.get(url, expect=200, auth=self.get_other_credentials())

View File

@@ -170,6 +170,7 @@ try:
# Support for get_description method on views compatible with 2.2.x. # Support for get_description method on views compatible with 2.2.x.
if hasattr(cls, 'get_description') and callable(cls.get_description): if hasattr(cls, 'get_description') and callable(cls.get_description):
desc = cls().get_description(html=html) desc = cls().get_description(html=html)
cls = type(cls.__name__, (object,), {'__doc__': desc})
elif hasattr(cls, 'view_description'): elif hasattr(cls, 'view_description'):
if callable(cls.view_description): if callable(cls.view_description):
view_desc = cls.view_description() view_desc = cls.view_description()

54
awx/main/utils.py Normal file
View File

@@ -0,0 +1,54 @@
# Python
import logging
import re
import sys
# Django
from django.conf import settings
from django.shortcuts import _get_queryset
# Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore']
def get_object_or_400(klass, *args, **kwargs):
'''
Return a single object from the given model or queryset based on the query
params, otherwise raise an exception that will return in a 400 response.
'''
queryset = _get_queryset(klass)
try:
return queryset.get(*args, **kwargs)
except queryset.model.DoesNotExist, e:
raise ParseError(*e.args)
except queryset.model.MultipleObjectsReturned, e:
raise ParseError(*e.args)
def get_object_or_403(klass, *args, **kwargs):
'''
Return a single object from the given model or queryset based on the query
params, otherwise raise an exception that will return in a 403 response.
'''
queryset = _get_queryset(klass)
try:
return queryset.get(*args, **kwargs)
except queryset.model.DoesNotExist, e:
raise PermissionDenied(*e.args)
except queryset.model.MultipleObjectsReturned, e:
raise PermissionDenied(*e.args)
def camelcase_to_underscore(s):
'''
Convert CamelCase names to lowercase_with_underscore.
'''
s = re.sub(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', s)
return s.lower().strip('_')
class RequireDebugTrueOrTest(logging.Filter):
'''
Logging filter to output when in DEBUG mode or running tests.
'''
def filter(self, record):
return settings.DEBUG or 'test' in sys.argv

View File

@@ -32,6 +32,7 @@ from awx.main.base_views import *
from awx.main.models import * from awx.main.models import *
from awx.main.permissions import * from awx.main.permissions import *
from awx.main.serializers import * from awx.main.serializers import *
from awx.main.utils import *
def handle_error(request, status=404): def handle_error(request, status=404):
context = {} context = {}
@@ -189,7 +190,6 @@ class OrganizationUsersList(SubListCreateAPIView):
serializer_class = UserSerializer serializer_class = UserSerializer
parent_model = Organization parent_model = Organization
relationship = 'users' relationship = 'users'
inject_primary_key_on_post_as = 'organization'
class OrganizationAdminsList(SubListCreateAPIView): class OrganizationAdminsList(SubListCreateAPIView):
@@ -197,8 +197,6 @@ class OrganizationAdminsList(SubListCreateAPIView):
serializer_class = UserSerializer serializer_class = UserSerializer
parent_model = Organization parent_model = Organization
relationship = 'admins' relationship = 'admins'
inject_primary_key_on_post_as = 'organization'
class OrganizationProjectsList(SubListCreateAPIView): class OrganizationProjectsList(SubListCreateAPIView):
@@ -206,7 +204,6 @@ class OrganizationProjectsList(SubListCreateAPIView):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = Organization parent_model = Organization
relationship = 'projects' relationship = 'projects'
inject_primary_key_on_post_as = 'organization'
class OrganizationTeamsList(SubListCreateAPIView): class OrganizationTeamsList(SubListCreateAPIView):
@@ -214,8 +211,7 @@ class OrganizationTeamsList(SubListCreateAPIView):
serializer_class = TeamSerializer serializer_class = TeamSerializer
parent_model = Organization parent_model = Organization
relationship = 'teams' relationship = 'teams'
inject_primary_key_on_post_as = 'organization' parent_key = 'organization'
severable = False
class TeamList(ListCreateAPIView): class TeamList(ListCreateAPIView):
@@ -233,8 +229,6 @@ class TeamUsersList(SubListCreateAPIView):
serializer_class = UserSerializer serializer_class = UserSerializer
parent_model = Team parent_model = Team
relationship = 'users' relationship = 'users'
inject_primary_key_on_post_as = 'team'
severable = True
class TeamPermissionsList(SubListCreateAPIView): class TeamPermissionsList(SubListCreateAPIView):
@@ -242,7 +236,7 @@ class TeamPermissionsList(SubListCreateAPIView):
serializer_class = PermissionSerializer serializer_class = PermissionSerializer
parent_model = Team parent_model = Team
relationship = 'permissions' relationship = 'permissions'
inject_primary_key_on_post_as = 'team' parent_key = 'team'
def get_queryset(self): def get_queryset(self):
# FIXME # FIXME
@@ -261,8 +255,6 @@ class TeamProjectsList(SubListCreateAPIView):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = Team parent_model = Team
relationship = 'projects' relationship = 'projects'
inject_primary_key_on_post_as = 'team'
severable = True
class TeamCredentialsList(SubListCreateAPIView): class TeamCredentialsList(SubListCreateAPIView):
@@ -270,7 +262,7 @@ class TeamCredentialsList(SubListCreateAPIView):
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
parent_model = Team parent_model = Team
relationship = 'credentials' relationship = 'credentials'
inject_primary_key_on_post_as = 'team' parent_key = 'team'
class ProjectList(ListCreateAPIView): class ProjectList(ListCreateAPIView):
@@ -293,7 +285,6 @@ class ProjectOrganizationsList(SubListCreateAPIView):
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
parent_model = Project parent_model = Project
relationship = 'organizations' relationship = 'organizations'
inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work?
def get_queryset(self): def get_queryset(self):
# FIXME # FIXME
@@ -308,7 +299,6 @@ class ProjectTeamsList(SubListCreateAPIView):
serializer_class = TeamSerializer serializer_class = TeamSerializer
parent_model = Project parent_model = Project
relationship = 'teams' relationship = 'teams'
inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work?
def get_queryset(self): def get_queryset(self):
project = Project.objects.get(pk=self.kwargs['pk']) project = Project.objects.get(pk=self.kwargs['pk'])
@@ -347,6 +337,7 @@ class UserTeamsList(SubListAPIView):
serializer_class = TeamSerializer serializer_class = TeamSerializer
parent_model = User parent_model = User
relationship = 'teams' relationship = 'teams'
parent_access = 'read'
class UserPermissionsList(SubListCreateAPIView): class UserPermissionsList(SubListCreateAPIView):
@@ -354,7 +345,8 @@ class UserPermissionsList(SubListCreateAPIView):
serializer_class = PermissionSerializer serializer_class = PermissionSerializer
parent_model = User parent_model = User
relationship = 'permissions' relationship = 'permissions'
inject_primary_key_on_post_as = 'user' parent_key = 'user'
parent_access = 'read'
class UserProjectsList(SubListAPIView): class UserProjectsList(SubListAPIView):
@@ -362,6 +354,7 @@ class UserProjectsList(SubListAPIView):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
parent_model = User parent_model = User
relationship = 'projects' relationship = 'projects'
parent_access = 'read'
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -375,7 +368,8 @@ class UserCredentialsList(SubListCreateAPIView):
serializer_class = CredentialSerializer serializer_class = CredentialSerializer
parent_model = User parent_model = User
relationship = 'credentials' relationship = 'credentials'
inject_primary_key_on_post_as = 'user' parent_key = 'user'
parent_access = 'read'
class UserOrganizationsList(SubListAPIView): class UserOrganizationsList(SubListAPIView):
@@ -383,6 +377,7 @@ class UserOrganizationsList(SubListAPIView):
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
parent_model = User parent_model = User
relationship = 'organizations' relationship = 'organizations'
parent_access = 'read'
class UserAdminOfOrganizationsList(SubListAPIView): class UserAdminOfOrganizationsList(SubListAPIView):
@@ -390,6 +385,7 @@ class UserAdminOfOrganizationsList(SubListAPIView):
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
parent_model = User parent_model = User
relationship = 'admin_of_organizations' relationship = 'admin_of_organizations'
parent_access = 'read'
class UserDetail(RetrieveUpdateDestroyAPIView): class UserDetail(RetrieveUpdateDestroyAPIView):
@@ -399,8 +395,9 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
def update_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 ''' ''' 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']) obj = User.objects.get(pk=kwargs['pk'])
can_change = request.user.can_access(User, 'change', obj, request.DATA)
can_admin = request.user.can_access(User, 'admin', obj, request.DATA) can_admin = request.user.can_access(User, 'admin', obj, request.DATA)
if not can_admin or can_admin == 'partial': if can_change and not can_admin:
admin_only_edit_fields = ('last_name', 'first_name', 'username', admin_only_edit_fields = ('last_name', 'first_name', 'username',
'is_active', 'is_superuser') 'is_active', 'is_superuser')
changed = {} changed = {}
@@ -412,10 +409,10 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
if changed: if changed:
raise PermissionDenied('Cannot change %s' % ', '.join(changed.keys())) raise PermissionDenied('Cannot change %s' % ', '.join(changed.keys()))
if 'password' in request.DATA and request.DATA['password']: new_password = request.DATA.get('password', '')
obj.set_password(request.DATA['password']) if can_change and new_password:
obj.set_password(new_password)
obj.save() obj.save()
request.DATA.pop('password')
class CredentialList(ListAPIView): class CredentialList(ListAPIView):
@@ -459,8 +456,7 @@ class InventoryHostsList(SubListCreateAPIView):
parent_model = Inventory parent_model = Inventory
relationship = 'hosts' relationship = 'hosts'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'inventory' parent_key = 'inventory'
severable = False
class HostGroupsList(SubListCreateAPIView): class HostGroupsList(SubListCreateAPIView):
''' the list of groups a host is directly a member of ''' ''' the list of groups a host is directly a member of '''
@@ -470,7 +466,6 @@ class HostGroupsList(SubListCreateAPIView):
parent_model = Host parent_model = Host
relationship = 'groups' relationship = 'groups'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'host'
class HostAllGroupsList(SubListAPIView): class HostAllGroupsList(SubListAPIView):
''' the list of all groups of which the host is directly or indirectly a member ''' ''' the list of all groups of which the host is directly or indirectly a member '''
@@ -500,7 +495,6 @@ class GroupChildrenList(SubListCreateAPIView):
parent_model = Group parent_model = Group
relationship = 'children' relationship = 'children'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'parent'
class GroupHostsList(SubListCreateAPIView): class GroupHostsList(SubListCreateAPIView):
''' the list of hosts directly below a group ''' ''' the list of hosts directly below a group '''
@@ -510,7 +504,6 @@ class GroupHostsList(SubListCreateAPIView):
parent_model = Group parent_model = Group
relationship = 'hosts' relationship = 'hosts'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'group'
class GroupAllHostsList(SubListAPIView): class GroupAllHostsList(SubListAPIView):
''' the list of all hosts below a group, even including subgroups ''' ''' the list of all hosts below a group, even including subgroups '''
@@ -540,8 +533,7 @@ class InventoryGroupsList(SubListCreateAPIView):
parent_model = Inventory parent_model = Inventory
relationship = 'groups' relationship = 'groups'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'inventory' parent_key = 'inventory'
severable = False
class InventoryRootGroupsList(SubListCreateAPIView): class InventoryRootGroupsList(SubListCreateAPIView):
@@ -550,8 +542,7 @@ class InventoryRootGroupsList(SubListCreateAPIView):
parent_model = Inventory parent_model = Inventory
relationship = 'groups' relationship = 'groups'
parent_access = 'read' parent_access = 'read'
inject_primary_key_on_post_as = 'inventory' parent_key = 'inventory'
severable = False
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -790,8 +781,7 @@ class JobTemplateJobsList(SubListCreateAPIView):
serializer_class = JobSerializer serializer_class = JobSerializer
parent_model = JobTemplate parent_model = JobTemplate
relationship = 'jobs' relationship = 'jobs'
inject_primary_key_on_post_as = 'job_template' parent_key = 'job_template'
severable = False
class JobList(ListCreateAPIView): class JobList(ListCreateAPIView):
@@ -955,7 +945,6 @@ class JobJobEventsList(BaseJobEventsList):
# in URL patterns and reverse URL lookups, converting CamelCase names to # in URL patterns and reverse URL lookups, converting CamelCase names to
# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). # lowercase_with_underscore (e.g. MyView.as_view() becomes my_view).
this_module = sys.modules[__name__] this_module = sys.modules[__name__]
camelcase_to_underscore = lambda str: re.sub(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', str).lower().strip('_')
for attr, value in locals().items(): for attr, value in locals().items():
if isinstance(value, type) and issubclass(value, APIView): if isinstance(value, type) and issubclass(value, APIView):
name = camelcase_to_underscore(attr) name = camelcase_to_underscore(attr)

View File

@@ -278,6 +278,9 @@ LOGGING = {
'require_debug_true': { 'require_debug_true': {
'()': 'awx.main.compat.RequireDebugTrue', '()': 'awx.main.compat.RequireDebugTrue',
}, },
'require_debug_true_or_test': {
'()': 'awx.main.utils.RequireDebugTrueOrTest',
},
}, },
'formatters': { 'formatters': {
'simple': { 'simple': {
@@ -287,7 +290,7 @@ LOGGING = {
'handlers': { 'handlers': {
'console': { 'console': {
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['require_debug_true'], 'filters': ['require_debug_true_or_test'],
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'simple', 'formatter': 'simple',
}, },
@@ -328,12 +331,10 @@ LOGGING = {
}, },
'awx.main.permissions': { 'awx.main.permissions': {
'handlers': ['null'], 'handlers': ['null'],
# Comment the line below to show lots of permissions logging.
'propagate': False, 'propagate': False,
}, },
'awx.main.access': { 'awx.main.access': {
'handlers': ['null'], 'handlers': ['null'],
# Comment the line below to show lots of permissions logging.
'propagate': False, 'propagate': False,
}, },
} }

View File

@@ -106,6 +106,11 @@ LOGGING['handlers']['syslog'] = {
# 'formatter': 'simple', # 'formatter': 'simple',
#} #}
# Enable the following lines to turn on lots of permissions-related logging.
#LOGGING['loggers']['awx.main.access']['propagate'] = True
#LOGGING['loggers']['awx.main.permissions']['propagate'] = True
# Define additional environment variables to be passed to subprocess started by # Define additional environment variables to be passed to subprocess started by
# the celery task. # the celery task.
#AWX_TASK_ENV['FOO'] = 'BAR' #AWX_TASK_ENV['FOO'] = 'BAR'