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',
#}
# 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
# the celery task.
#AWX_TASK_ENV['FOO'] = 'BAR'

View File

@ -13,6 +13,7 @@ from django.contrib.auth.models import User
from rest_framework.exceptions import PermissionDenied
# AWX
from awx.main.utils import *
from awx.main.models import *
from awx.main.licenses import LicenseReader
@ -140,15 +141,25 @@ class BaseAccess(object):
return self.can_change(obj, None)
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
def get_queryset(self):
# I can see user records when I'm a superuser, I'm that user, I'm
# their org admin, or I'm on a team with that user.
qs = self.model.objects.filter(is_active=True).distinct()
if self.user.is_superuser:
return self.model.objects.all()
return self.model.objects.filter(is_active=True).filter(
return qs
return qs.filter(
Q(pk=self.user.pk) |
Q(organizations__in=self.user.admin_of_organizations.all()) |
Q(teams__in=self.user.teams.all())
@ -167,31 +178,40 @@ class UserAccess(BaseAccess):
def can_change(self, obj, data):
# 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:
return True
if self.user == obj:
return 'partial'
return bool(obj.organizations.filter(admins__in=[self.user]).count())
def can_delete(self, obj):
if obj == self.user:
# cannot delete yourself
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:
# cannot delete the last superuser
# cannot delete the last active superuser
return False
return bool(self.user.is_superuser or
obj.organizations.filter(admins__in=[self.user]).count())
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
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
@ -203,146 +223,103 @@ class OrganizationAccess(BaseAccess):
def can_change(self, obj, data):
return bool(self.user.is_superuser or
obj.created_by == self.user or
self.user in obj.admins.all())
def can_delete(self, obj):
return self.can_change(obj, None)
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
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()
def get_queryset(self, allowed=None):
allowed = allowed or PERMISSION_TYPES_ALLOWING_INVENTORY_READ
qs = Inventory.objects.filter(active=True).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,
return qs
admin_of = qs.filter(organization__admins__in=[self.user]).distinct()
has_user_perms = qs.filter(
permissions__user__in=[self.user],
permissions__permission_type__in=allowed,
).distinct()
has_team_perms = base.filter(
permissions__team__in = self.user.teams.all(),
permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
has_team_perms = qs.filter(
permissions__team__users__in=[self.user],
permissions__permission_type__in=allowed,
).distinct()
return admin_of | has_user_perms | has_team_perms
def _has_permission_types(self, obj, allowed):
if self.user.is_superuser:
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 has_permission_types(self, obj, allowed):
return bool(obj and self.get_queryset(allowed).filter(pk=obj.pk).count())
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):
if not 'organization' in data:
return True
# If no data is specified, just checking for generic add permission?
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:
return True
if not self.user.is_superuser:
org = Organization.objects.get(pk=data['organization'])
if self.user in org.admins.all():
else:
org = get_object_or_400(Organization, pk=data.get('organization', None))
if self.user.can_access(Organization, 'change', org, None):
return True
return False
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):
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):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN)
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)
return self.can_admin(obj, None)
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
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
qs = self.model.objects.filter(active=True).distinct()
inventories_qs = self.user.get_queryset(Inventory)
return qs.filter(inventory__in=inventories_qs)
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):
if not 'inventory' in data:
if not data or not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on inventory.
permissions_ok = self.user.can_access(Inventory, 'change', inventory, None)
if not permissions_ok:
inventory = get_object_or_400(Inventory, pk=data.get('inventory', None))
if not self.user.can_access(Inventory, 'change', inventory, None):
return False
# 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)
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
# 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):
'''
I can see groups whenever I can see their inventory.
I can change or delete groups whenever I can change their inventory.
'''
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
qs = self.model.objects.filter(active=True).distinct()
inventories_qs = self.user.get_queryset(Inventory)
return qs.filter(inventory__in=inventories_qs)
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):
if not 'inventory' in data:
if not data or not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['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)
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 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,
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
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):
@ -425,22 +391,33 @@ class GroupAccess(BaseAccess):
return True
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
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()
qs = self.model.objects.filter(active=True).distinct()
if self.user.is_superuser:
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):
return self.can_change(obj, None)
return obj and self.can_change(obj, None)
def can_add(self, data):
if self.user.is_superuser:
@ -476,7 +453,17 @@ class TeamAccess(BaseAccess):
model = Team
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):
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
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()
if self.user.is_superuser:
return qs.all()
return qs.filter(active=True).filter(
qs = qs.filter(active=True).filter(
Q(project__organizations__admins__in=[self.user]) |
Q(project__teams__users__in=[self.user])
).distinct()
#print qs.values_list('name', flat=True)
return qs
def can_read(self, obj):
# 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):
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):
# 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):
# Base class for a read-only sublist view.
@ -47,6 +61,18 @@ class SubListAPIView(ListAPIView):
# to view sublist):
# 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):
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
@ -70,13 +96,23 @@ class SubListAPIView(ListAPIView):
sublist_qs = getattr(parent, self.relationship).distinct()
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
# 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
# parent_key = 'field_on_model_referring_to_parent'
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):
# 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
# 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
# inject additional data.
if hasattr(request.DATA, 'dict'):
@ -98,8 +127,10 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
else:
data = request.DATA
# add the key to the post data using the pk from the URL
data[inject_primary_key] = kwargs['pk']
# add the parent key to the post data using the pk from the URL
parent_key = getattr(self, 'parent_key', None)
if parent_key:
data[parent_key] = self.kwargs['pk']
# attempt to deserialize the object
serializer = self.serializer_class(data=data)
@ -160,7 +191,7 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
return Response(data, status=status.HTTP_400_BAD_REQUEST)
parent = self.get_parent_object()
severable = getattr(self, 'severable', True)
parent_key = getattr(self, 'parent_key', None)
relationship = getattr(parent, self.relationship)
try:
@ -172,11 +203,12 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
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
if parent_key:
# sub object has a ForeignKey to the parent, so we can't remove it
# from the set, only mark it as inactive.
sub.mark_inactive()
else:
relationship.remove(sub)
return Response(status=status.HTTP_204_NO_CONTENT)
@ -186,7 +218,6 @@ class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
else:
return self.attach(request, *args, **kwargs)
class RetrieveAPIView(generics.RetrieveAPIView):
pass

View File

@ -1,16 +1,17 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Django
from django.core.exceptions import FieldError
# Django REST Framework
from rest_framework.exceptions import ParseError
from rest_framework.filters import BaseFilterBackend
class DefaultFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
terms = {}
order_by = None
# Filtering by is_active/active that was previously in BaseList.
qs = queryset
for field in queryset.model._meta.fields:
@ -19,25 +20,34 @@ class DefaultFilterBackend(BaseFilterBackend):
elif field.name == 'active':
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' ]:
continue
filters = {}
order_by = None
if key in ('order', 'order_by'):
order_by = value
continue
for key, value in request.QUERY_PARAMS.items():
key2 = key
if key2.endswith("__int"):
key2 = key.replace("__int","")
value = int(value)
if key in ('page', 'page_size', 'format'):
continue
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:
qs = qs.order_by(order_by)
filters[key] = value
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

View File

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

View File

@ -124,13 +124,15 @@ class BaseTestMixin(object):
self.normal_password = 'normal'
self.other_username = '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)
if not just_super_user:
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.nobody_django_user = self.make_user(self.nobody_username, self.nobody_password, super_user=False)
def get_super_credentials(self):
return (self.super_username, self.super_password)
@ -141,6 +143,10 @@ class BaseTestMixin(object):
def get_other_credentials(self):
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):
return ('random', 'combination')
@ -239,6 +245,39 @@ class BaseTestMixin(object):
data = self.get(collection_url, expect=200, auth=auth)
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):
'''
Base class for unit tests.

View File

@ -33,15 +33,137 @@ class InventoryTest(BaseTest):
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')
self.nobody_django_user.set_password('nobody')
self.nobody_django_user.save()
# Check list view with invalid authentication.
self.check_invalid_auth(url)
def get_nobody_credentials(self):
# here is a user without any permissions...
return ('nobody', 'nobody')
# a super user can list all inventories
self.check_get_list(url, self.super_django_user, qs)
# 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):
@ -53,57 +175,57 @@ class InventoryTest(BaseTest):
groups = reverse('main:group_list')
# a super user can list inventories
data = self.get(inventories, expect=200, auth=self.get_super_credentials())
self.assertEquals(data['count'], 2)
#data = self.get(inventories, expect=200, auth=self.get_super_credentials())
#self.assertEquals(data['count'], 2)
# an org admin can list inventories but is filtered to what he adminsters
data = self.get(inventories, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 1)
#data = self.get(inventories, expect=200, auth=self.get_normal_credentials())
#self.assertEquals(data['count'], 1)
# 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())
self.assertEquals(data['count'], 1)
#data = self.get(inventories, expect=200, auth=self.get_other_credentials())
#self.assertEquals(data['count'], 1)
# a regular user not part of anything cannot see any inventories
data = self.get(inventories, expect=200, auth=self.get_nobody_credentials())
self.assertEquals(data['count'], 0)
#data = self.get(inventories, expect=200, auth=self.get_nobody_credentials())
#self.assertEquals(data['count'], 0)
# a super user can get inventory records
data = self.get(inventories_1, expect=200, auth=self.get_super_credentials())
self.assertEquals(data['name'], 'inventory-a')
#data = self.get(inventories_1, expect=200, auth=self.get_super_credentials())
#self.assertEquals(data['name'], 'inventory-a')
# an org admin can get inventory records
data = self.get(inventories_1, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['name'], 'inventory-a')
#data = self.get(inventories_1, expect=200, auth=self.get_normal_credentials())
#self.assertEquals(data['name'], 'inventory-a')
# 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_2, expect=200, auth=self.get_other_credentials())
self.assertEquals(data['name'], 'inventory-b')
#data = self.get(inventories_1, expect=403, auth=self.get_other_credentials())
#data = self.get(inventories_2, expect=200, auth=self.get_other_credentials())
#self.assertEquals(data['name'], 'inventory-b')
# a regular user cannot read any inventory records
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_1, 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
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
data = self.post(inventories, data=new_inv_1, expect=201, auth=self.get_super_credentials())
self.assertEquals(data['id'], new_id)
#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
#data = self.post(inventories, data=new_inv_1, expect=201, auth=self.get_super_credentials())
#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')
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_incomplete = dict(name='inventory-d', description='baz')
#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)
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)
data = self.post(inventories, data=new_inv_my_org, expect=201, 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)
#data = self.post(inventories, data=new_inv_my_org, expect=201, auth=self.get_normal_credentials())
# a regular user cannot create inventory
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())
#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())
# a super user can add hosts (but inventory ID is required)
inv = Inventory.objects.create(

View File

@ -12,7 +12,7 @@ import uuid
from django.contrib.auth.models import User as DjangoUser
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Q
import django.test
from django.test.client import Client
from django.test.utils import override_settings
@ -427,57 +427,49 @@ class BaseJobTestMixin(BaseTestMixin):
super(BaseJobTestMixin, self).setUp()
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):
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):
url = reverse('main:job_template_list')
qs = JobTemplate.objects.distinct()
fields = self.JOB_TEMPLATE_FIELDS
# 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
with self.current_user(self.user_sue):
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)
# Sue's credentials (superuser) == 200, full list
self.check_get_list(url, self.user_sue, qs, fields)
# Alex's credentials (admin of all orgs) == 200, full list
self.check_get_list(url, self.user_alex, qs, fields)
# FIXME: Check individual job template result fields.
# alex's credentials (admin of all orgs) == 200, full list
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
# Bob's credentials (admin of eng, user of ops) == 200, all from
# engineering and operations.
with self.current_user(self.user_bob):
self.options(url)
self.head(url)
response = self.get(url)
qs = JobTemplate.objects.filter(
inventory__organization__in=[self.org_eng, self.org_ops],
bob_qs = qs.filter(
Q(project__organizations__admins__in=[self.user_bob]) |
Q(project__teams__users__in=[self.user_bob]),
)
#self.check_pagination_and_size(response, qs.count())
#self.check_list_ids(response, qs)
self.check_get_list(url, self.user_bob, bob_qs, fields)
# 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.
@ -492,7 +484,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
)
# 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.
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,))
# 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.
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,))
# 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.
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,))
# 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.
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.
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.
with self.current_user(self.user_sue):
@ -615,7 +607,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
url = reverse('main:job_list')
# 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
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.
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.
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,))
# Test with no auth and with invalid login.
self._test_invalid_creds(url)
self.check_invalid_auth(url)
# sue can read the job detail.
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,))
# 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.
self.assertEqual(job.status, 'new')
@ -780,8 +772,8 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
url = reverse('main:job_start', args=(job.pk,))
# Test with no auth and with invalid login.
self._test_invalid_creds(url)
self._test_invalid_creds(url, methods=('post',))
self.check_invalid_auth(url)
self.check_invalid_auth(url, methods=('post',))
# Sue can start a job (when passwords are already saved) as long as the
# 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,))
# Test with no auth and with invalid login.
self._test_invalid_creds(url)
self._test_invalid_creds(url, methods=('post',))
self.check_invalid_auth(url)
self.check_invalid_auth(url, methods=('post',))
# sue can cancel the job, but only when it is pending or running.
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.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 write_test_file(project, name, content):
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=403, auth=self.get_nobody_credentials())
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())
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())
self.assertEqual(my_teams1['count'], 2)
self.assertEqual(my_teams1, my_teams2)
self.assertEqual(my_teams2['count'], 2)
# =====================================================================
# USER PROJECTS
@ -511,14 +505,14 @@ class ProjectsTest(BaseTest):
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())
# 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
url = cred_put_u['url']
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())
# 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())
# =====================================================================

View File

@ -142,7 +142,7 @@ class UsersTest(BaseTest):
def test_user_list_filtered(self):
url = reverse('main:user_list')
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())
self.assertEquals(data2['count'], 2)
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.
if hasattr(cls, 'get_description') and callable(cls.get_description):
desc = cls().get_description(html=html)
cls = type(cls.__name__, (object,), {'__doc__': desc})
elif hasattr(cls, 'view_description'):
if callable(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.permissions import *
from awx.main.serializers import *
from awx.main.utils import *
def handle_error(request, status=404):
context = {}
@ -189,7 +190,6 @@ class OrganizationUsersList(SubListCreateAPIView):
serializer_class = UserSerializer
parent_model = Organization
relationship = 'users'
inject_primary_key_on_post_as = 'organization'
class OrganizationAdminsList(SubListCreateAPIView):
@ -197,8 +197,6 @@ class OrganizationAdminsList(SubListCreateAPIView):
serializer_class = UserSerializer
parent_model = Organization
relationship = 'admins'
inject_primary_key_on_post_as = 'organization'
class OrganizationProjectsList(SubListCreateAPIView):
@ -206,7 +204,6 @@ class OrganizationProjectsList(SubListCreateAPIView):
serializer_class = ProjectSerializer
parent_model = Organization
relationship = 'projects'
inject_primary_key_on_post_as = 'organization'
class OrganizationTeamsList(SubListCreateAPIView):
@ -214,8 +211,7 @@ class OrganizationTeamsList(SubListCreateAPIView):
serializer_class = TeamSerializer
parent_model = Organization
relationship = 'teams'
inject_primary_key_on_post_as = 'organization'
severable = False
parent_key = 'organization'
class TeamList(ListCreateAPIView):
@ -233,8 +229,6 @@ class TeamUsersList(SubListCreateAPIView):
serializer_class = UserSerializer
parent_model = Team
relationship = 'users'
inject_primary_key_on_post_as = 'team'
severable = True
class TeamPermissionsList(SubListCreateAPIView):
@ -242,7 +236,7 @@ class TeamPermissionsList(SubListCreateAPIView):
serializer_class = PermissionSerializer
parent_model = Team
relationship = 'permissions'
inject_primary_key_on_post_as = 'team'
parent_key = 'team'
def get_queryset(self):
# FIXME
@ -261,8 +255,6 @@ class TeamProjectsList(SubListCreateAPIView):
serializer_class = ProjectSerializer
parent_model = Team
relationship = 'projects'
inject_primary_key_on_post_as = 'team'
severable = True
class TeamCredentialsList(SubListCreateAPIView):
@ -270,7 +262,7 @@ class TeamCredentialsList(SubListCreateAPIView):
serializer_class = CredentialSerializer
parent_model = Team
relationship = 'credentials'
inject_primary_key_on_post_as = 'team'
parent_key = 'team'
class ProjectList(ListCreateAPIView):
@ -293,7 +285,6 @@ class ProjectOrganizationsList(SubListCreateAPIView):
serializer_class = OrganizationSerializer
parent_model = Project
relationship = 'organizations'
inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work?
def get_queryset(self):
# FIXME
@ -308,7 +299,6 @@ class ProjectTeamsList(SubListCreateAPIView):
serializer_class = TeamSerializer
parent_model = Project
relationship = 'teams'
inject_primary_key_on_post_as = 'project' # Not correct, but needed for the post to work?
def get_queryset(self):
project = Project.objects.get(pk=self.kwargs['pk'])
@ -347,6 +337,7 @@ class UserTeamsList(SubListAPIView):
serializer_class = TeamSerializer
parent_model = User
relationship = 'teams'
parent_access = 'read'
class UserPermissionsList(SubListCreateAPIView):
@ -354,7 +345,8 @@ class UserPermissionsList(SubListCreateAPIView):
serializer_class = PermissionSerializer
parent_model = User
relationship = 'permissions'
inject_primary_key_on_post_as = 'user'
parent_key = 'user'
parent_access = 'read'
class UserProjectsList(SubListAPIView):
@ -362,6 +354,7 @@ class UserProjectsList(SubListAPIView):
serializer_class = ProjectSerializer
parent_model = User
relationship = 'projects'
parent_access = 'read'
def get_queryset(self):
parent = self.get_parent_object()
@ -375,7 +368,8 @@ class UserCredentialsList(SubListCreateAPIView):
serializer_class = CredentialSerializer
parent_model = User
relationship = 'credentials'
inject_primary_key_on_post_as = 'user'
parent_key = 'user'
parent_access = 'read'
class UserOrganizationsList(SubListAPIView):
@ -383,6 +377,7 @@ class UserOrganizationsList(SubListAPIView):
serializer_class = OrganizationSerializer
parent_model = User
relationship = 'organizations'
parent_access = 'read'
class UserAdminOfOrganizationsList(SubListAPIView):
@ -390,6 +385,7 @@ class UserAdminOfOrganizationsList(SubListAPIView):
serializer_class = OrganizationSerializer
parent_model = User
relationship = 'admin_of_organizations'
parent_access = 'read'
class UserDetail(RetrieveUpdateDestroyAPIView):
@ -399,8 +395,9 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
def update_filter(self, request, *args, **kwargs):
''' make sure non-read-only fields that can only be edited by admins, are only edited by admins '''
obj = User.objects.get(pk=kwargs['pk'])
can_change = request.user.can_access(User, 'change', 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',
'is_active', 'is_superuser')
changed = {}
@ -412,10 +409,10 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
if changed:
raise PermissionDenied('Cannot change %s' % ', '.join(changed.keys()))
if 'password' in request.DATA and request.DATA['password']:
obj.set_password(request.DATA['password'])
new_password = request.DATA.get('password', '')
if can_change and new_password:
obj.set_password(new_password)
obj.save()
request.DATA.pop('password')
class CredentialList(ListAPIView):
@ -459,8 +456,7 @@ class InventoryHostsList(SubListCreateAPIView):
parent_model = Inventory
relationship = 'hosts'
parent_access = 'read'
inject_primary_key_on_post_as = 'inventory'
severable = False
parent_key = 'inventory'
class HostGroupsList(SubListCreateAPIView):
''' the list of groups a host is directly a member of '''
@ -470,7 +466,6 @@ class HostGroupsList(SubListCreateAPIView):
parent_model = Host
relationship = 'groups'
parent_access = 'read'
inject_primary_key_on_post_as = 'host'
class HostAllGroupsList(SubListAPIView):
''' 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
relationship = 'children'
parent_access = 'read'
inject_primary_key_on_post_as = 'parent'
class GroupHostsList(SubListCreateAPIView):
''' the list of hosts directly below a group '''
@ -510,7 +504,6 @@ class GroupHostsList(SubListCreateAPIView):
parent_model = Group
relationship = 'hosts'
parent_access = 'read'
inject_primary_key_on_post_as = 'group'
class GroupAllHostsList(SubListAPIView):
''' the list of all hosts below a group, even including subgroups '''
@ -540,8 +533,7 @@ class InventoryGroupsList(SubListCreateAPIView):
parent_model = Inventory
relationship = 'groups'
parent_access = 'read'
inject_primary_key_on_post_as = 'inventory'
severable = False
parent_key = 'inventory'
class InventoryRootGroupsList(SubListCreateAPIView):
@ -550,8 +542,7 @@ class InventoryRootGroupsList(SubListCreateAPIView):
parent_model = Inventory
relationship = 'groups'
parent_access = 'read'
inject_primary_key_on_post_as = 'inventory'
severable = False
parent_key = 'inventory'
def get_queryset(self):
parent = self.get_parent_object()
@ -790,8 +781,7 @@ class JobTemplateJobsList(SubListCreateAPIView):
serializer_class = JobSerializer
parent_model = JobTemplate
relationship = 'jobs'
inject_primary_key_on_post_as = 'job_template'
severable = False
parent_key = 'job_template'
class JobList(ListCreateAPIView):
@ -955,7 +945,6 @@ class JobJobEventsList(BaseJobEventsList):
# in URL patterns and reverse URL lookups, converting CamelCase names to
# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view).
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():
if isinstance(value, type) and issubclass(value, APIView):
name = camelcase_to_underscore(attr)

View File

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

View File

@ -106,6 +106,11 @@ LOGGING['handlers']['syslog'] = {
# '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
# the celery task.
#AWX_TASK_ENV['FOO'] = 'BAR'