Split out RBAC and can_user_* methods from models into access.py. Moved list/item permissions checks from the base views into RBAC. Added serializers/views/tests for jobs REST API.

This commit is contained in:
Chris Church 2013-05-08 10:46:16 -04:00
parent 75ea4a1cda
commit 2a15d07221
12 changed files with 1481 additions and 874 deletions

548
lib/main/access.py Normal file
View File

@ -0,0 +1,548 @@
import logging
from django.db.models import Q
from django.contrib.auth.models import User
from lib.main.models import *
__all__ = ['get_user_queryset', 'check_user_access']
logger = logging.getLogger('lib.main.access')
access_registry = {
# <model_class>: [<access_class>, ...],
# ...
}
def register_access(model_class, access_class):
access_classes = access_registry.setdefault(model_class, [])
access_classes.append(access_class)
def get_user_queryset(user, model_class):
'''
Return a queryset for the given model_class containing only the instances
that should be visible to the given user.
'''
querysets = []
for access_class in access_registry.get(model_class, []):
access_instance = access_class(user)
querysets.append(access_instance.get_queryset())
if not querysets:
return model_class.objects.none()
elif len(querysets) == 1:
return querysets[0]
else:
queryset = model_class.objects.all()
for qs in querysets:
queryset = queryset.filter(pk__in=qs.values_list('pk', flat=True))
return queryset
def check_user_access(user, model_class, action, *args, **kwargs):
'''
Return True if user can perform action against model_class with the
provided parameters.
'''
for access_class in access_registry.get(model_class, []):
access_instance = access_class(user)
access_method = getattr(access_instance, 'can_%s' % action, None)
if not access_method:
continue
result = access_method(*args, **kwargs)
logger.debug('%s.%s %r returned %r', access_instance.__class__.__name__,
access_method.__name__, args, result)
if result:
return result
return False
class BaseAccess(object):
model = None
def __init__(self, user):
self.user = user
def get_queryset(self):
if self.user.is_superuser:
return self.model.objects.all()
else:
return self.model.objects.none()
def can_read(self, obj):
return self.user.is_superuser
def can_add(self, data):
return self.user.is_superuser
def can_change(self, obj, data):
return self.user.is_superuser
def can_write(self, obj, data):
# Alias for change.
return self.can_change(obj, data)
def can_admin(self, obj, data):
# Alias for can_change. Can be overridden if admin vs. user change
# permissions need to be different.
return self.can_change(obj, data)
def can_delete(self, obj):
return self.user.is_superuser
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
if skip_sub_obj_read_check:
return self.can_change(obj, None)
else:
return bool(self.can_change(obj, None) and
check_user_access(self.user, type(sub_obj), 'read',
sub_obj))
def can_unattach(self, obj, sub_obj, relationship):
return self.can_change(obj, None)
class UserAccess(BaseAccess):
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.
if self.user.is_superuser:
return self.model.objects.all()
return self.model.objects.filter(is_active=True).filter(
Q(pk=self.user.pk) |
Q(organizations__in=self.user.admin_of_organizations.all()) |
Q(teams__in=self.request.user.teams.all())
).distinct()
def can_read(self, obj):
# A user can be read if they are on the same team or can be changed.
matching_teams = self.user.teams.filter(users__in=[self.user]).count()
return bool(matching_teams or self.can_change(obj, None))
def can_add(self, data):
# TODO: reuse. make helper functions like "is user an org admin"
# apply throughout permissions code
return bool(self.user.is_superuser or
self.user.admin_of_organizations.count())
def can_change(self, obj, data):
# A user can be changed if they are themselves, or by org admins or
# superusers.
if self.user == obj:
return 'partial'
return bool(self.user.is_superuser or
obj.organizations.filter(admins__in=[self.user]).count())
def can_delete(self, obj):
return bool(self.user.is_superuser or
obj.organizations.filter(admins__in=[self.user]).count())
class TagAccess(BaseAccess):
model = Tag
def can_read(self, obj):
# anybody can read tags, we won't show much detail other than the names
return True
def can_add(self, data):
# anybody can make up tags
return True
class OrganizationAccess(BaseAccess):
model = Organization
def can_read(self, obj):
return bool(self.can_change(obj, None) or
self.user in obj.users.all())
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):
model = Inventory
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 can_read(self, obj):
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_READ)
def can_add(self, data):
if not 'organization' in data:
return True
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():
return True
return False
def can_change(self, obj, data):
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)
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 check_user_access(self.user, 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):
model = Host
def can_read(self, obj):
return check_user_access(self.user, Inventory, 'read', obj.inventory)
def can_add(self, data):
if not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on inventory.
return check_user_access(self.user, Inventory, 'change', inventory, None)
class GroupAccess(BaseAccess):
model = Group
def can_read(self, obj):
return check_user_access(self.user, Inventory, 'read', obj.inventory)
def can_add(self, data):
if not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['inventory'])
# Checks for admin or change permission on inventory.
return check_user_access(self.user, Inventory, 'change', inventory, None)
def can_change(self, obj, data):
# Checks for admin or change permission on inventory, controls whether
# the user can attach subgroups
return check_user_access(self.user, Inventory, 'change', obj.inventory, None)
class VariableDataAccess(BaseAccess):
model = VariableData
def can_read(self, obj):
if obj.host:
inventory = obj.host.inventory
elif obj.group:
inventory = obj.group.inventory
else:
return False
return check_user_access(self.user, Inventory, 'read', inventory)
def can_change(self, obj, data):
if obj.host:
inventory = obj.host.inventory
elif obj.group:
inventory = obj.group.inventory
else:
return False
return check_user_access(self.user, Inventory, 'change', inventory)
def can_delete(self, obj):
return False
class CredentialAccess(BaseAccess):
model = Credential
def can_read(self, obj):
return self.can_change(obj, None)
def can_add(self, data):
if self.user.is_superuser:
return True
if 'user' in data:
user_obj = User.objects.get(pk=data['user'])
return check_user_access(self.user, User, 'change', user_obj, None)
if 'team' in data:
team_obj = Team.objects.get(pk=data['team'])
return check_user_access(self.user, Team, 'change', team_obj, None)
def can_change(self, obj, data):
if self.user.is_superuser:
return True
if self.user == obj.user:
return True
if obj.user:
if (obj.user.organizations.filter(admins__in = [self.user]).count()):
return True
if obj.team:
if self.user in obj.team.organization.admins.all():
return True
return False
def can_delete(self, obj):
if obj.user is None and obj.team is None:
# unassociated credentials may be marked deleted by anyone
return True
return self.can_change(obj, None)
class TeamAccess(BaseAccess):
model = Team
def can_add(self, data):
if self.user.is_superuser:
return True
if Organization.objects.filter(admins__in = [self.user]).count():
# team assignment to organizations is handled elsewhere, this just creates
# a blank team
return True
return False
def can_read(self, obj):
if self.can_change(obj, None):
return True
if obj.users.filter(pk__in = [ self.user.pk ]).count():
return True
return False
def can_change(self, obj, data):
# FIXME -- audit when this is called explicitly, if any
if self.user.is_superuser:
return True
if self.user in obj.organization.admins.all():
return True
return False
def can_delete(self, obj):
return self.can_change(obj, None)
class ProjectAccess(BaseAccess):
model = Project
def can_read(self, obj):
if self.can_change(obj, None):
return True
# and also if I happen to be on a team inside the project
# FIXME: add this too
return False
def can_change(self, obj, data):
if self.user.is_superuser:
return True
if obj.created_by == self.user:
return True
organizations = Organization.objects.filter(admins__in = [ self.user ], projects__in = [ obj ])
for org in organizations:
if org in project.organizations():
return True
return False
def can_delete(self, obj):
return self.can_change(obj, None)
class PermissionAccess(BaseAccess):
model = Permission
def can_read(self, obj):
# a permission can be seen by the assigned user or team
# or anyone who can administrate that permission
if obj.user and obj.user == self.user:
return True
if obj.team and obj.team.users.filter(pk = self.user.pk).count() > 0:
return True
return self.can_change(obj, None)
def can_change(self, obj, data):
if self.user.is_superuser:
return True
# a permission can be administrated by a super
# or if a user permission, that an admin of a user's organization
# or if a team permission, an admin of that team's organization
if obj.user and obj.user.organizations.filter(admins__in = [self.user]).count() > 0:
return True
if obj.team and obj.team.organization.admins.filter(user=self.user).count() > 0:
return True
return False
def can_delete(self, obj):
return self.can_change(obj, None)
class JobTemplateAccess(BaseAccess):
model = JobTemplate
def get_queryset(self):
'''
I can see job templates when I am a superuser, or I am an admin of the
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.
'''
qs = self.model.objects.all()
if self.user.is_superuser:
return qs.all()
return qs.filter(active=True).filter(
Q(project__organizations__admins__in=[self.user]) |
Q(project__teams__users__in=[self.user])
).distinct()
def can_read(self, obj):
# you can only see the job templates that you have permission to launch.
data = dict(
inventory = obj.inventory.pk,
project = obj.project.pk,
job_type = obj.job_type
)
return self.can_add(data)
def can_add(self, data):
'''
a user can create a job template if they are a superuser, an org admin of any org
that the project is a member, or if they have user or team based permissions tying
the project to the inventory source for the given action.
users who are able to create deploy jobs can also make check (dry run) jobs
'''
if self.user.is_superuser:
return True
if not data or '_method' in data: # FIXME: So the browseable API will work?
return True
project = Project.objects.get(pk=data['project'])
inventory = Inventory.objects.get(pk=data['inventory'])
admin_of_orgs = project.organizations.filter(admins__in = [ self.user ])
if admin_of_orgs.count() > 0:
return True
job_type = data['job_type']
has_launch_permission = False
user_permissions = Permission.objects.filter(inventory=inventory, project=project, user=self.user)
for perm in user_permissions:
if job_type == PERM_INVENTORY_CHECK:
# if you have run permissions, you can also create check jobs
has_launch_permission = True
elif job_type == PERM_INVENTORY_DEPLOY and perm.permission_type == PERM_INVENTORY_DEPLOY:
# you need explicit run permissions to make run jobs
has_launch_permission = True
team_permissions = Permission.objects.filter(inventory=inventory, project=project, team__users__in = [self.user])
for perm in team_permissions:
if job_type == PERM_INVENTORY_CHECK:
# if you have run permissions, you can also create check jobs
has_launch_permission = True
elif job_type == PERM_INVENTORY_DEPLOY and perm.permission_type == PERM_INVENTORY_DEPLOY:
# you need explicit run permissions to make run jobs
has_launch_permission = True
if not has_launch_permission:
return False
# make sure user owns the credentials they are using
if data.has_key('credential'):
has_credential = False
credential = Credential.objects.get(pk=data['credential'])
if credential.team and credential.team.users.filter(id = self.user.pk).count():
has_credential = True
if credential.user and credential.user == self.user:
has_credential = True
if not has_credential:
return False
# shouldn't really matter with permissions given, but make sure the user
# is also currently on the team in case they were added a per-user permission and then removed
# from the project.
if project.teams.filter(users__in = [ self.user ]).count():
return False
return True
def can_change(self, obj, data):
'''
'''
return False # FIXME
class JobAccess(BaseAccess):
model = Job
def can_start(self, obj):
return False # FIXME
def can_cancel(self, obj):
return False # FIXME
class JobHostSummaryAccess(BaseAccess):
model = JobHostSummary
class JobEventAccess(BaseAccess):
model = JobEvent
register_access(User, UserAccess)
register_access(Tag, TagAccess)
register_access(Organization, OrganizationAccess)
register_access(Inventory, InventoryAccess)
register_access(Host, HostAccess)
register_access(Group, GroupAccess)
register_access(VariableData, VariableDataAccess)
register_access(Credential, CredentialAccess)
register_access(Team, TeamAccess)
register_access(Project, ProjectAccess)
register_access(Permission, PermissionAccess)
register_access(JobTemplate, JobTemplateAccess)
register_access(Job, JobAccess)
register_access(JobHostSummary, JobHostSummaryAccess)
register_access(JobEvent, JobEventAccess)

View File

@ -20,6 +20,7 @@ from lib.main.models import *
from django.contrib.auth.models import User
from lib.main.serializers import *
from lib.main.rbac import *
from lib.main.access import *
from rest_framework.exceptions import PermissionDenied
from rest_framework import mixins
from rest_framework import generics
@ -34,23 +35,11 @@ import json as python_json
class BaseList(generics.ListCreateAPIView):
def list_permissions_check(self, request, obj=None):
''' determines some early yes/no access decisions, pre-filtering '''
#print '---', request.method, getattr(request, '_method', None)
if request.method in ('OPTIONS', 'HEAD', 'GET'):
return True
if request.method == 'POST':
if self.__class__.model in [ User ]:
ok = request.user.is_superuser or (request.user.admin_of_organizations.count() > 0)
if not ok:
raise PermissionDenied()
return True
else:
# audit all of these to check ownership/readability of subobjects
if not self.__class__.model.can_user_add(request.user, self.request.DATA):
raise PermissionDenied()
return True
return False#raise exceptions.NotImplementedError
permission_classes = (CustomRbac,)
# Subclasses should define:
# model = ModelClass
# serializer_class = SerializerClass
def get_queryset(self):
@ -59,7 +48,7 @@ class BaseList(generics.ListCreateAPIView):
qs = None
if model == User:
qs = base.filter(is_active=True)
elif model in [ Tag, AuditTrail ]:
elif model in [ Tag, AuditTrail, JobEvent ]:
qs = base
else:
qs = self._get_queryset().filter(active=True)
@ -70,22 +59,19 @@ class BaseList(generics.ListCreateAPIView):
return qs
class BaseSubList(BaseList):
''' used for subcollections with an overriden post '''
def list_permissions_check(self, request, obj=None):
''' determines some early yes/no access decisions, pre-filtering '''
if request.method in ('OPTIONS', 'HEAD', 'GET'):
return True
if request.method == 'POST':
# the can_user_attach methods will be called below
return True
raise exceptions.NotImplementedError
# Subclasses should define at least:
# model = ModelClass
# serializer_class = SerializerClass
# parent_model = ModelClass
# relationship = 'rel_name_from_parent_to_model'
# And optionally:
# postable = True/False
# inject_primary_key_on_post_as = 'field_on_model_referring_to_parent'
# severable = True/False
def post(self, request, *args, **kwargs):
@ -93,8 +79,14 @@ class BaseSubList(BaseList):
if not postable:
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
# Make a copy of the data provided (since it's readonly) in order to
# inject additional data.
if hasattr(request.DATA, 'dict'):
data = request.DATA.dict()
else:
data = request.DATA
parent_id = kwargs['pk']
sub_id = request.DATA.get('id', None)
sub_id = data.get('id', None)
main = self.__class__.parent_model.objects.get(pk=parent_id)
severable = getattr(self.__class__, 'severable', True)
@ -103,7 +95,7 @@ class BaseSubList(BaseList):
if sub_id:
subs = self.__class__.model.objects.filter(pk=sub_id)
else:
if 'disassociate' in request.DATA:
if 'disassociate' in data:
raise PermissionDenied() # ID is required to disassociate
else:
@ -117,20 +109,22 @@ class BaseSubList(BaseList):
if inject_primary_key is not None:
# add the key to the post data using the pk from the URL
request.DATA[inject_primary_key] = kwargs['pk']
data[inject_primary_key] = kwargs['pk']
# attempt to deserialize the object
ser = self.__class__.serializer_class(data=request.DATA)
ser = self.__class__.serializer_class(data=data)
if not ser.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST, data=ser.errors)
# ask the usual access control settings
if self.__class__.model in [ User ]:
if not UserHelper.can_user_add(request.user, ser.init_data):
raise PermissionDenied()
else:
if not self.__class__.model.can_user_add(request.user, ser.init_data):
raise PermissionDenied()
#if self.__class__.model in [ User ]:
# if not UserHelper.can_user_add(request.user, ser.init_data):
# raise PermissionDenied()
#else:
# if not self.__class__.model.can_user_add(request.user, ser.init_data):
# raise PermissionDenied()
if not check_user_access(request.user, self.model, 'add', ser.init_data):
raise PermissionDenied()
# save the object through the serializer, reload and returned the saved object deserialized
obj = ser.save()
@ -154,23 +148,26 @@ class BaseSubList(BaseList):
# model so we have to cheat here. This may happen for other cases
# where we are creating a user immediately on a subcollection
# when that user does not already exist. Relations will work post-save.
organization = Organization.objects.get(pk=request.DATA[inject_primary_key])
organization = Organization.objects.get(pk=data[inject_primary_key])
if not request.user.is_superuser:
if not organization.admins.filter(pk=request.user.pk).count() > 0:
raise PermissionDenied()
else:
raise exceptions.NotImplementedError()
else:
if not obj.__class__.can_user_read(request.user, obj):
#if not obj.__class__.can_user_read(request.user, obj):
if not check_user_access(request.user, type(obj), 'read', obj):
raise PermissionDenied()
if not self.__class__.parent_model.can_user_attach(request.user, main, obj, self.__class__.relationship, request.DATA):
#if not self.__class__.parent_model.can_user_attach(request.user, main, obj, self.__class__.relationship, data):
# If we just created a new object, we may not yet be able to read it because it's not yet associated with its parent model.
if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data, skip_sub_obj_read_check=True):
raise PermissionDenied()
# FIXME: manual attachment code neccessary for users here, move this into the main code.
# this is because users don't have FKs into what they are attaching. (also refactor)
if self.__class__.parent_model == Organization:
organization = Organization.objects.get(pk=request.DATA[inject_primary_key])
organization = Organization.objects.get(pk=data[inject_primary_key])
import lib.main.views
if self.__class__ == lib.main.views.OrganizationsUsersList:
organization.users.add(obj)
@ -178,10 +175,12 @@ class BaseSubList(BaseList):
organization.admins.add(obj)
else:
if not UserHelper.can_user_read(request.user, obj):
#if not UserHelper.can_user_read(request.user, obj):
if not check_user_access(request.user, type(obj), 'read', obj):
raise PermissionDenied()
# FIXME: should generalize this
if not UserHelper.can_user_attach(request.user, main, obj, self.__class__.relationship, request.DATA):
#if not UserHelper.can_user_attach(request.user, main, obj, self.__class__.relationship, data):
if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data):
raise PermissionDenied()
return Response(status=status.HTTP_201_CREATED, data=ser.data)
@ -199,13 +198,15 @@ class BaseSubList(BaseList):
sub = subs[0]
relationship = getattr(main, self.__class__.relationship)
if not 'disassociate' in request.DATA:
if not 'disassociate' in data:
if not request.user.is_superuser:
if type(main) != User:
if not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship, request.DATA):
#if not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship, data):
if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data):
raise PermissionDenied()
else:
if not UserHelper.can_user_attach(request.user, main, sub, self.__class__.relationship, request.DATA):
#if not UserHelper.can_user_attach(request.user, main, sub, self.__class__.relationship, data):
if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data):
raise PermissionDenied()
if sub in relationship.all():
@ -214,10 +215,12 @@ class BaseSubList(BaseList):
else:
if not request.user.is_superuser:
if type(main) != User:
if not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship):
#if not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship):
if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship):
raise PermissionDenied()
else:
if not UserHelper.can_user_unattach(request.user, main, sub, self.__class__.relationship):
#if not UserHelper.can_user_unattach(request.user, main, sub, self.__class__.relationship):
if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship):
raise PermissionDenied()
@ -240,11 +243,13 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView):
def destroy(self, request, *args, **kwargs):
# somewhat lame that delete has to call it's own permissions check
obj = self.model.objects.get(pk=kwargs['pk'])
# FIXME: Why isn't the active check being caught earlier by RBAC?
if getattr(obj, 'active', True) == False:
raise Http404()
if getattr(obj, 'is_active', True) == False:
raise Http404()
if not request.user.is_superuser and not self.delete_permissions_check(request, obj):
#if not request.user.is_superuser and not self.delete_permissions_check(request, obj):
if not check_user_access(request.user, self.model, 'delete', obj):
raise PermissionDenied()
if isinstance(obj, PrimordialModel):
obj.name = "_deleted_%s_%s" % (str(datetime.time()), obj.name)
@ -258,28 +263,6 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView):
raise Exception("InternalError: destroy() not implemented yet for %s" % obj)
return HttpResponse(status=204)
def delete_permissions_check(self, request, obj):
if isinstance(obj, PrimordialModel):
return self.__class__.model.can_user_delete(request.user, obj)
elif isinstance(obj, User):
return UserHelper.can_user_delete(request.user, obj)
raise PermissionDenied()
def item_permissions_check(self, request, obj):
if request.method == 'GET':
if type(obj) == User:
return UserHelper.can_user_read(request.user, obj)
else:
return self.__class__.model.can_user_read(request.user, obj)
elif request.method in [ 'PUT' ]:
if type(obj) == User:
return UserHelper.can_user_administrate(request.user, obj, request.DATA)
else:
return self.__class__.model.can_user_administrate(request.user, obj, request.DATA)
return False
def put(self, request, *args, **kwargs):
self.put_filter(request, *args, **kwargs)
return super(BaseDetail, self).put(request, *args, **kwargs)
@ -297,26 +280,16 @@ class VariableBaseDetail(BaseDetail):
def destroy(self, request, *args, **kwargs):
raise PermissionDenied()
def delete_permissions_check(self, request, obj):
raise PermissionDenied()
def item_permissions_check(self, request, obj):
through_obj = self.__class__.parent_model.objects.get(pk = self.request.args['pk'])
if request.method == 'GET':
return self.__class__.parent_model.can_user_read(request.user, through_obj)
elif request.method in [ 'PUT' ]:
return self.__class__.parent_model.can_user_administrate(request.user, through_obj, request.DATA)
return False
def put(self, request, *args, **kwargs):
# FIXME: lots of overlap between put and get here, need to refactor
through_obj = self.__class__.parent_model.objects.get(pk=kwargs['pk'])
has_permission = Inventory._has_permission_types(request.user, through_obj.inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
if not has_permission:
raise PermissionDenied()
#has_permission = Inventory._has_permission_types(request.user, through_obj.inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
#if not has_permission:
# raise PermissionDenied()
if not check_user_access(request.user, Inventory, 'change', through_obj.inventory, None):
raise PermissionDenied
this_object = None
@ -354,8 +327,11 @@ class VariableBaseDetail(BaseDetail):
setattr(through_obj, self.__class__.reverse_relationship, this_object)
through_obj.save()
has_permission = Inventory._has_permission_types(request.user, through_obj.inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
if not has_permission:
raise PermissionDenied()
#has_permission = Inventory._has_permission_types(request.user, through_obj.inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
#if not has_permission:
# raise PermissionDenied()
if not check_user_access(request.user, Inventory, 'read', through_obj.inventory):
raise PermissionDenied
return Response(status=status.HTTP_200_OK, data=python_json.loads(this_object.data))

View File

@ -81,7 +81,9 @@ class EditHelper(object):
@classmethod
def illegal_changes(cls, request, obj, model_class):
''' have any illegal changes been made (for a PUT request)? '''
can_admin = model_class.can_user_administrate(request.user, obj, request.DATA)
from lib.main.access import *
#can_admin = model_class.can_user_administrate(request.user, obj, request.DATA)
can_admin = check_user_access(request.user, User, 'change', obj, request.DATA)
if (not can_admin) or (can_admin == 'partial'):
check_fields = model_class.admin_only_edit_fields
changed = cls.fields_changed(check_fields, obj, request.DATA)
@ -107,51 +109,6 @@ class UserHelper(object):
# fields that the user themselves cannot edit, but are not actually read only
admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active', 'is_superuser')
@classmethod
def can_user_administrate(cls, user, obj, data):
''' a user can be administrated if they are themselves, or by org admins or superusers '''
if user == obj:
return 'partial'
if user.is_superuser:
return True
matching_orgs = obj.organizations.filter(admins__in = [user]).count()
return matching_orgs
@classmethod
def can_user_read(cls, user, obj):
''' a user can be read if they are on the same team or can be administrated '''
matching_teams = user.teams.filter(users__in = [ user ]).count()
return matching_teams or cls.can_user_administrate(user, obj, None)
@classmethod
def can_user_delete(cls, user, obj):
if user.is_superuser:
return True
matching_orgs = obj.organizations.filter(admins__in = [user]).count()
return matching_orgs
@classmethod
def can_user_add(cls, user, data):
# TODO: reuse. make helper functions like "is user an org admin"
# apply throughout permissions code
if user.is_superuser:
return True
return user.admin_of_organizations.count() > 0
@classmethod
def can_user_attach(cls, user, obj, sub_obj, relationship_type, data):
if type(sub_obj) != User:
if not sub_obj.can_user_read(user, sub_obj):
return False
rc = cls.can_user_administrate(user, obj, None)
if not rc:
return False
return sub_obj.__class__.can_user_read(user, sub_obj)
@classmethod
def can_user_unattach(cls, user, obj, sub_obj, relationship_type):
return cls.can_user_administrate(user, obj, None)
class PrimordialModel(models.Model):
'''
common model for all object types that have these standard fields
@ -165,6 +122,8 @@ class PrimordialModel(models.Model):
description = models.TextField(blank=True, default='')
created_by = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, related_name='%s(class)s_created', editable=False) # not blank=False on purpose for admin!
creation_date = models.DateField(auto_now_add=True)
#created = models.DateTimeField(auto_now_add=True)
#modified = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField('Tag', related_name='%(class)s_by_tag', blank=True)
audit_trail = models.ManyToManyField('AuditTrail', related_name='%(class)s_by_audit_trail', blank=True)
active = models.BooleanField(default=True)
@ -172,45 +131,6 @@ class PrimordialModel(models.Model):
def __unicode__(self):
return unicode("%s-%s"% (self.name, self.id))
@classmethod
def can_user_administrate(cls, user, obj, data):
# FIXME: do we want a seperate method to override put? This is kind of general purpose
raise Exception("can_user_administrate needs to be implemented in model subclass")
@classmethod
def can_user_delete(cls, user, obj):
raise Exception("can_user_delete needs to be implemented in model subclass")
@classmethod
def can_user_read(cls, user, obj):
raise Exception("can_user_read needs to be implemented in model subclass")
@classmethod
def can_user_add(cls, user, data):
return user.is_superuser
@classmethod
def can_user_attach(cls, user, obj, sub_obj, relationship_type, data):
''' whether you can add sub_obj to obj using the relationship type in a subobject view '''
if type(sub_obj) != User:
if not sub_obj.can_user_read(user, sub_obj):
return False
rc = cls.can_user_administrate(user, obj, None)
if not rc:
return False
# in order to attach something you also be able to read what you are attaching
if type(sub_obj) == User:
# we already check that the user is an admin or org admin up in base_views.py
# because the user doesn't have the attributes on it directly to tie it to the org
return True
else:
return sub_obj.__class__.can_user_read(user, sub_obj)
@classmethod
def can_user_unattach(cls, user, obj, sub_obj, relationship):
return cls.can_user_administrate(user, obj, None)
class CommonModel(PrimordialModel):
''' a base model where the name is unique '''
@ -243,17 +163,6 @@ class Tag(models.Model):
def get_absolute_url(self):
return reverse('main:tags_detail', args=(self.pk,))
@classmethod
def can_user_add(cls, user, data):
# anybody can make up tags
return True
@classmethod
def can_user_read(cls, user, obj):
# anybody can read tags, we won't show much detail other than the names
return True
class AuditTrail(models.Model):
'''
changing any object records the change
@ -286,28 +195,6 @@ class Organization(CommonModel):
def get_absolute_url(self):
return reverse('main:organizations_detail', args=(self.pk,))
@classmethod
def can_user_delete(cls, user, obj):
return user in obj.admins.all()
@classmethod
def can_user_administrate(cls, user, obj, data):
# FIXME: super user checks should be higher up so we don't have to repeat them
if user.is_superuser:
return True
if obj.created_by == user:
return True
rc = user in obj.admins.all()
return rc
@classmethod
def can_user_read(cls, user, obj):
return cls.can_user_administrate(user,obj,None) or user in obj.users.all()
@classmethod
def can_user_delete(cls, user, obj):
return cls.can_user_administrate(user, obj, None)
def __unicode__(self):
return self.name
@ -326,82 +213,6 @@ class Inventory(CommonModel):
def get_absolute_url(self):
return reverse('main:inventory_detail', args=(self.pk,))
@classmethod
def _has_permission_types(cls, user, obj, allowed):
if user.is_superuser:
return True
by_org_admin = obj.organization.admins.filter(pk = user.pk).count()
by_team_permission = obj.permissions.filter(
team__in = user.teams.all(),
permission_type__in = allowed
).count()
by_user_permission = obj.permissions.filter(
user = user,
permission_type__in = allowed
).count()
result = (by_org_admin + by_team_permission + by_user_permission)
return result > 0
@classmethod
def _has_any_inventory_permission_types(cls, user, 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 user.is_superuser:
return True
by_org_admin = user.organizations.filter(
admins__in = [ user ]
).count()
by_team_permission = Permission.objects.filter(
team__in = user.teams.all(),
permission_type__in = allowed
).count()
by_user_permission = user.permissions.filter(
permission_type__in = allowed
).count()
result = (by_org_admin + by_team_permission + by_user_permission)
return result > 0
@classmethod
def can_user_add(cls, user, data):
if not 'organization' in data:
return True
if user.is_superuser:
return True
if not user.is_superuser:
org = Organization.objects.get(pk=data['organization'])
if user in org.admins.all():
return True
return False
@classmethod
def can_user_administrate(cls, user, obj, data):
return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN)
@classmethod
def can_user_attach(cls, user, obj, sub_obj, relationship_type, data):
''' 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):
return False
return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
@classmethod
def can_user_unattach(cls, user, obj, sub_obj, relationship):
return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
@classmethod
def can_user_read(cls, user, obj):
return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_READ)
@classmethod
def can_user_delete(cls, user, obj):
return cls._has_permission_types(user, obj, PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN)
class Host(CommonModelNameNotUnique):
'''
A managed node
@ -417,18 +228,6 @@ class Host(CommonModelNameNotUnique):
def __unicode__(self):
return self.name
@classmethod
def can_user_read(cls, user, obj):
return Inventory.can_user_read(user, obj.inventory)
@classmethod
def can_user_add(cls, user, data):
if not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['inventory'])
rc = Inventory._has_permission_types(user, inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
return rc
def get_absolute_url(self):
return reverse('main:hosts_detail', args=(self.pk,))
@ -453,23 +252,6 @@ class Group(CommonModelNameNotUnique):
def __unicode__(self):
return self.name
@classmethod
def can_user_add(cls, user, data):
if not 'inventory' in data:
return False
inventory = Inventory.objects.get(pk=data['inventory'])
return Inventory._has_permission_types(user, inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
@classmethod
def can_user_administrate(cls, user, obj, data):
# here this controls whether the user can attach subgroups
return Inventory._has_permission_types(user, obj.inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
@classmethod
def can_user_read(cls, user, obj):
return Inventory.can_user_read(user, obj.inventory)
def get_absolute_url(self):
return reverse('main:groups_detail', args=(self.pk,))
@ -495,15 +277,6 @@ class VariableData(CommonModelNameNotUnique):
def get_absolute_url(self):
return reverse('main:variable_detail', args=(self.pk,))
@classmethod
def can_user_read(cls, user, obj):
''' a user can be read if they are on the same team or can be administrated '''
if obj.host is not None:
return Inventory.can_user_read(user, obj.host.inventory)
if obj.group is not None:
return Inventory.can_user_read(user, obj.group.inventory)
return False
class Credential(CommonModelNameNotUnique):
'''
A credential contains information about how to talk to a remote set of hosts
@ -571,44 +344,6 @@ class Credential(CommonModelNameNotUnique):
def needs_sudo_password(self):
return self.sudo_password == 'ASK'
@classmethod
def can_user_administrate(cls, user, obj, data):
if user.is_superuser:
return True
if user == obj.user:
return True
if obj.user:
if (obj.user.organizations.filter(admins__in = [user]).count()):
return True
if obj.team:
if user in obj.team.organization.admins.all():
return True
return False
@classmethod
def can_user_delete(cls, user, obj):
if obj.user is None and obj.team is None:
# unassociated credentials may be marked deleted by anyone
return True
return cls.can_user_administrate(user,obj,None)
@classmethod
def can_user_read(cls, user, obj):
''' a user can be read if they are on the same team or can be administrated '''
return cls.can_user_administrate(user, obj, None)
@classmethod
def can_user_add(cls, user, data):
if user.is_superuser:
return True
if 'user' in data:
user_obj = User.objects.get(pk=data['user'])
return UserHelper.can_user_administrate(user, user_obj, data)
if 'team' in data:
team_obj = Team.objects.get(pk=data['team'])
return Team.can_user_administrate(user, team_obj, data)
def get_absolute_url(self):
return reverse('main:credentials_detail', args=(self.pk,))
@ -627,37 +362,6 @@ class Team(CommonModel):
def get_absolute_url(self):
return reverse('main:teams_detail', args=(self.pk,))
@classmethod
def can_user_administrate(cls, user, obj, data):
# FIXME -- audit when this is called explicitly, if any
if user.is_superuser:
return True
if user in obj.organization.admins.all():
return True
return False
@classmethod
def can_user_read(cls, user, obj):
if cls.can_user_administrate(user, obj, None):
return True
if obj.users.filter(pk__in = [ user.pk ]).count():
return True
return False
@classmethod
def can_user_add(cls, user, data):
if user.is_superuser:
return True
if Organization.objects.filter(admins__in = [user]).count():
# team assignment to organizations is handled elsewhere, this just creates
# a blank team
return True
return False
@classmethod
def can_user_delete(cls, user, obj):
return cls.can_user_administrate(user, obj, None)
class Project(CommonModel):
'''
A project represents a playbook git repo that can access a set of inventories
@ -682,30 +386,6 @@ class Project(CommonModel):
def get_absolute_url(self):
return reverse('main:projects_detail', args=(self.pk,))
@classmethod
def can_user_administrate(cls, user, obj, data):
if user.is_superuser:
return True
if obj.created_by == user:
return True
organizations = Organization.objects.filter(admins__in = [ user ], projects__in = [ obj ])
for org in organizations:
if org in project.organizations():
return True
return False
@classmethod
def can_user_read(cls, user, obj):
if cls.can_user_administrate(user, obj, None):
return True
# and also if I happen to be on a team inside the project
# FIXME: add this too
return False
@classmethod
def can_user_delete(cls, user, obj):
return cls.can_user_administrate(user, obj, None)
@property
def available_playbooks(self):
playbooks = []
@ -780,33 +460,6 @@ class Permission(CommonModelNameNotUnique):
def get_absolute_url(self):
return reverse('main:permissions_detail', args=(self.pk,))
@classmethod
def can_user_administrate(cls, user, obj, data):
if user.is_superuser:
return True
# a permission can be administrated by a super
# or if a user permission, that an admin of a user's organization
# or if a team permission, an admin of that team's organization
if obj.user and obj.user.organizations.filter(admins__in = [user]).count() > 0:
return True
if obj.team and obj.team.organization.admins.filter(user=user).count() > 0:
return True
return False
@classmethod
def can_user_read(cls, user, obj):
# a permission can be seen by the assigned user or team
# or anyone who can administrate that permission
if obj.user and obj.user == user:
return True
if obj.team and obj.team.users.filter(pk = user.pk).count() > 0:
return True
return cls.can_user_administrate(user, obj, None)
@classmethod
def can_user_delete(cls, user, obj):
return cls.can_user_administrate(user, obj, None)
# TODO: other job types (later)
class JobTemplate(CommonModel):
@ -892,85 +545,7 @@ class JobTemplate(CommonModel):
return job
def get_absolute_url(self):
return reverse('main:job_templates_detail', args=(self.pk,))
@classmethod
def can_user_read(cls, user, obj):
# you can only see the job templates that you have permission to launch.
data = dict(
inventory = obj.inventory.pk,
project = obj.project.pk,
job_type = obj.job_type
)
return cls.can_user_add(user, data)
@classmethod
def can_user_administrate(cls, user, obj, data):
'''
'''
@classmethod
def can_user_add(cls, user, data):
'''
a user can create a job template if they are a superuser, an org admin of any org
that the project is a member, or if they have user or team based permissions tying
the project to the inventory source for the given action.
users who are able to create deploy jobs can also make check (dry run) jobs
'''
if user.is_superuser:
return True
if not data or '_method' in data: # FIXME: So the browseable API will work?
return True
project = Project.objects.get(pk=data['project'])
inventory = Inventory.objects.get(pk=data['inventory'])
admin_of_orgs = project.organizations.filter(admins__in = [ user ])
if admin_of_orgs.count() > 0:
return True
job_type = data['job_type']
has_launch_permission = False
user_permissions = Permission.objects.filter(inventory=inventory, project=project, user=user)
for perm in user_permissions:
if job_type == PERM_INVENTORY_CHECK:
# if you have run permissions, you can also create check jobs
has_launch_permission = True
elif job_type == PERM_INVENTORY_DEPLOY and perm.permission_type == PERM_INVENTORY_DEPLOY:
# you need explicit run permissions to make run jobs
has_launch_permission = True
team_permissions = Permission.objects.filter(inventory=inventory, project=project, team__users__in = [user])
for perm in team_permissions:
if job_type == PERM_INVENTORY_CHECK:
# if you have run permissions, you can also create check jobs
has_launch_permission = True
elif job_type == PERM_INVENTORY_DEPLOY and perm.permission_type == PERM_INVENTORY_DEPLOY:
# you need explicit run permissions to make run jobs
has_launch_permission = True
if not has_launch_permission:
return False
# make sure user owns the credentials they are using
if data.has_key('credential'):
has_credential = False
credential = Credential.objects.get(pk=data['credential'])
if credential.team and credential.team.users.filter(id = user.pk).count():
has_credential = True
if credential.user and credential.user == user:
has_credential = True
if not has_credential:
return False
# shouldn't really matter with permissions given, but make sure the user
# is also currently on the team in case they were added a per-user permission and then removed
# from the project.
if project.teams.filter(users__in = [ user ]).count():
return False
return True
return reverse('main:job_template_detail', args=(self.pk,))
class Job(CommonModel):
'''
@ -1086,7 +661,7 @@ class Job(CommonModel):
)
def get_absolute_url(self):
return reverse('main:jobs_detail', args=(self.pk,))
return reverse('main:job_detail', args=(self.pk,))
@property
def celery_task(self):
@ -1248,6 +823,9 @@ class JobEvent(models.Model):
on_delete=models.SET_NULL,
)
def get_absolute_url(self):
return reverse('main:job_event_detail', args=(self.pk,))
def __unicode__(self):
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())

View File

@ -19,6 +19,7 @@ import logging
from django.http import Http404
from rest_framework.exceptions import PermissionDenied
from rest_framework import permissions
from lib.main.access import *
logger = logging.getLogger('lib.main.rbac')
@ -26,6 +27,48 @@ logger = logging.getLogger('lib.main.rbac')
class CustomRbac(permissions.BasePermission):
def _check_options_permissions(self, request, view, obj=None):
return self._check_get_permissions(request, view, obj)
def _check_head_permissions(self, request, view, obj=None):
return self._check_get_permissions(request, view, obj)
def _check_get_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
if not check_user_access(request.user, view.parent_model, 'read',
parent_obj):
return False
if not obj:
return True
return check_user_access(request.user, view.model, 'read', obj)
def _check_post_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
#if not check_user_access(request.user, view.parent_model, 'change',
# parent_obj, None):
# return False
# FIXME: attach/unattach
return True
else:
if obj:
return True
return check_user_access(request.user, view.model, 'add', request.DATA)
def _check_put_permissions(self, request, view, obj=None):
if not obj:
return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None?
return check_user_access(request.user, view.model, 'change', obj,
request.DATA)
def _check_delete_permissions(self, request, view, obj=None):
if not obj:
return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None?
return check_user_access(request.user, view.model, 'delete', obj)
def _check_permissions(self, request, view, obj=None):
# Check that obj (if given) is active, otherwise raise a 404.
active = getattr(obj, 'active', getattr(obj, 'is_active', True))
@ -42,6 +85,15 @@ class CustomRbac(permissions.BasePermission):
# Always allow superusers (as long as they are active).
if request.user.is_superuser:
return True
# Check permissions for the given view and object, based on the request
# method used.
check_method = getattr(self, '_check_%s_permissions' % \
request.method.lower(), None)
result = check_method and check_method(request, view, obj)
if not result:
raise PermissionDenied()
return result
# If no obj is given, check list permissions.
if obj is None:
if getattr(view, 'list_permissions_check', None):

View File

@ -14,33 +14,91 @@
# You should have received a copy of the GNU General Public License
# along with Ansible Commander. If not, see <http://www.gnu.org/licenses/>.
# Django
from django.contrib.auth.models import User
from lib.main.models import *
from rest_framework import serializers, pagination
from django.core.urlresolvers import reverse
from django.core.serializers import json
# Django REST framework
from rest_framework import serializers, pagination
from rest_framework.templatetags.rest_framework import replace_query_param
# Ansible Commander
from lib.main.models import *
BASE_FIELDS = ('id', 'url', 'related', 'creation_date', 'name', 'description')
class NextPageField(pagination.NextPageField):
def to_native(self, value):
if not value.has_next():
return None
page = value.next_page_number()
request = self.context.get('request')
url = request and request.get_full_path() or ''
return replace_query_param(url, self.page_field, page)
class PreviousPageField(pagination.NextPageField):
def to_native(self, value):
if not value.has_previous():
return None
page = value.previous_page_number()
request = self.context.get('request')
url = request and request.get_full_path() or ''
return replace_query_param(url, self.page_field, page)
class PaginationSerializer(pagination.BasePaginationSerializer):
'''
Custom pagination serializer to output only URL path (without host/port).
'''
count = serializers.Field(source='paginator.count')
next = NextPageField(source='*')
previous = PreviousPageField(source='*')
class BaseSerializer(serializers.ModelSerializer):
pass
class OrganizationSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
url = serializers.SerializerMethodField('get_absolute_url')
related = serializers.SerializerMethodField('get_related')
# make certain fields read only
creation_date = serializers.DateTimeField(read_only=True) # FIXME: is model Date or DateTime, fix model
active = serializers.BooleanField(read_only=True)
creation_date = serializers.SerializerMethodField('get_creation_date') # FIXME: is model Date or DateTime, fix model
active = serializers.SerializerMethodField('get_active')
def get_absolute_url(self, obj):
if isinstance(obj, User):
return reverse('main:users_detail', args=(obj.pk,))
else:
return obj.get_absolute_url()
def get_related(self, obj):
res = dict()
if getattr(obj, 'created_by', None):
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
return res
def get_creation_date(self, obj):
if isinstance(obj, User):
return obj.date_joined.date()
else:
return obj.creation_date
def get_active(self, obj):
if isinstance(obj, User):
return obj.is_active
else:
return obj.active
class OrganizationSerializer(BaseSerializer):
class Meta:
model = Organization
fields = ('url', 'id', 'name', 'description', 'creation_date', 'related') # whitelist
fields = BASE_FIELDS
def get_related(self, obj):
''' related resource URLs '''
res = dict(
res = super(OrganizationSerializer, self).get_related(obj)
res.update(dict(
audit_trail = reverse('main:organizations_audit_trail_list', args=(obj.pk,)),
projects = reverse('main:organizations_projects_list', args=(obj.pk,)),
inventories = reverse('main:organizations_inventories_list', args=(obj.pk,)),
@ -48,181 +106,137 @@ class OrganizationSerializer(BaseSerializer):
admins = reverse('main:organizations_admins_list', args=(obj.pk,)),
tags = reverse('main:organizations_tags_list', args=(obj.pk,)),
teams = reverse('main:organizations_teams_list', args=(obj.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
))
return res
class AuditTrailSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = AuditTrail
fields = ('url', 'id', 'modified_by', 'delta', 'detail', 'comment')
def get_related(self, obj):
res = dict()
res = super(AuditTrailSerializer, self).get_related(obj)
if obj.modified_by:
res['modified_by'] = reverse('main:users_detail', args=(obj.modified_by.pk,))
return res
class ProjectSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
available_playbooks = serializers.Field(source='available_playbooks')
class Meta:
model = Project
fields = ('url', 'id', 'name', 'description', 'creation_date', 'local_path')#, 'default_playbook', 'scm_type')
fields = BASE_FIELDS + ('local_path', 'available_playbooks')
# 'default_playbook', 'scm_type')
def get_related(self, obj):
res = dict(
res = super(ProjectSerializer, self).get_related(obj)
res.update(dict(
organizations = reverse('main:projects_organizations_list', args=(obj.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
))
return res
class InventorySerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Inventory
fields = ('url', 'id', 'name', 'description', 'creation_date', 'related', 'organization')
fields = BASE_FIELDS + ('organization',)
def get_related(self, obj):
res = dict(
res = super(InventorySerializer, self).get_related(obj)
res.update(dict(
hosts = reverse('main:inventory_hosts_list', args=(obj.pk,)),
groups = reverse('main:inventory_groups_list', args=(obj.pk,)),
organization = reverse('main:organizations_detail', args=(obj.organization.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
))
return res
class HostSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Host
fields = ('url', 'id', 'name', 'description', 'creation_date', 'related', 'inventory')
fields = BASE_FIELDS + ('inventory',)
def get_related(self, obj):
res = dict(
res = super(HostSerializer, self).get_related(obj)
res.update(dict(
variable_data = reverse('main:hosts_variable_detail', args=(obj.pk,)),
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
)
job_events = reverse('main:host_job_event_list', args=(obj.pk,)),
))
# NICE TO HAVE: possible reverse resource to show what groups the host is in
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
return res
class GroupSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Group
fields = ('url', 'id', 'name', 'description', 'creation_date', 'inventory')
fields = BASE_FIELDS + ('inventory',)
def get_related(self, obj):
res = dict(
res = super(GroupSerializer, self).get_related(obj)
res.update(dict(
variable_data = reverse('main:groups_variable_detail', args=(obj.pk,)),
hosts = reverse('main:groups_hosts_list', args=(obj.pk,)),
children = reverse('main:groups_children_list', args=(obj.pk,)),
all_hosts = reverse('main:groups_all_hosts_list', args=(obj.pk,)),
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
))
return res
class TeamSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Team
fields = ('url', 'id', 'related', 'name', 'description', 'organization', 'creation_date')
# FIXME: TODO: include related collections but also related FK urls
fields = BASE_FIELDS + ('organization',)
def get_related(self, obj):
res = dict(
res = super(TeamSerializer, self).get_related(obj)
res.update(dict(
projects = reverse('main:teams_projects_list', args=(obj.pk,)),
users = reverse('main:teams_users_list', args=(obj.pk,)),
credentials = reverse('main:teams_credentials_list', args=(obj.pk,)),
organization = reverse('main:organizations_detail', args=(obj.organization.pk,)),
permissions = reverse('main:teams_permissions_list', args=(obj.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
))
return res
class PermissionSerializer(BaseSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Permission
fields = ( 'url', 'id', 'user', 'team', 'name', 'description', 'creation_date',
'project', 'inventory', 'permission_type' )
fields = BASE_FIELDS + ('user', 'team', 'project', 'inventory',
'permission_type',)
def get_related(self, obj):
res = dict()
res = super(PermissionSerializer, self).get_related(obj)
if obj.user:
res['user'] = reverse('main:users_detail', args=(obj.user.pk,))
if obj.team:
res['team'] = reverse('main:teams_detail', args=(obj.team.pk,))
if self.project:
if obj.project:
res['project'] = reverse('main:projects_detail', args=(obj.project.pk,))
if self.inventory:
if obj.inventory:
res['inventory'] = reverse('main:inventory_detail', args=(obj.inventory.pk,))
if self.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
return res
class CredentialSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
# FIXME: may want to make some of these filtered based on user accessing
class Meta:
model = Credential
fields = (
'url', 'id', 'related', 'name', 'description', 'creation_date',
'ssh_username', 'ssh_password', 'ssh_key_data', 'ssh_key_unlock',
'sudo_username', 'sudo_password', 'user', 'team',
)
fields = BASE_FIELDS + ('ssh_username', 'ssh_password', 'ssh_key_data',
'ssh_key_unlock', 'sudo_username',
'sudo_password', 'user', 'team',)
def get_related(self, obj):
# FIXME: no related collections, do want to add user and team if defined
res = dict(
)
res = super(CredentialSerializer, self).get_related(obj)
if obj.user:
res['user'] = reverse('main:users_detail', args=(obj.user.pk,))
res['user'] = reverse('main:users_detail', args=(obj.user.pk,))
if obj.team:
res['team'] = reverse('main:teams_detail', args=(obj.team.pk,))
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
res['team'] = reverse('main:teams_detail', args=(obj.team.pk,))
return res
def validate(self, attrs):
@ -237,97 +251,122 @@ class CredentialSerializer(BaseSerializer):
class UserSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.SerializerMethodField('get_absolute_url_override')
related = serializers.SerializerMethodField('get_related')
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email', 'is_active', 'is_superuser', 'related')
fields = ('id', 'url', 'related', 'creation_date', 'username',
'first_name', 'last_name', 'email', 'is_active',
'is_superuser',)
def get_related(self, obj):
return dict(
res = super(UserSerializer, self).get_related(obj)
res.update(dict(
teams = reverse('main:users_teams_list', args=(obj.pk,)),
organizations = reverse('main:users_organizations_list', args=(obj.pk,)),
admin_of_organizations = reverse('main:users_admin_organizations_list', args=(obj.pk,)),
projects = reverse('main:users_projects_list', args=(obj.pk,)),
credentials = reverse('main:users_credentials_list', args=(obj.pk,)),
permissions = reverse('main:users_permissions_list', args=(obj.pk,)),
)
def get_absolute_url_override(self, obj):
return reverse('main:users_detail', args=(obj.pk,))
))
return res
class TagSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = Tag
fields = ('url', 'id', 'name')
fields = ('id', 'url', 'name')
def get_related(self, obj):
res = dict()
res = super(TagSerializer, self).get_related(obj)
res.pop('created_by', None)
return res
class VariableDataSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = VariableData
fields = ('url', 'id', 'data', 'related', 'name', 'description', 'creation_date')
fields = BASE_FIELDS + ('data',)
def get_related(self, obj):
# FIXME: add host or group if defined
res = dict(
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
res = super(VariableDataSerializer, self).get_related(obj)
try:
res['host'] = reverse('main:hosts_detail', args=(obj.host.pk,))
except Host.DoesNotExist:
pass
try:
res['group'] = reverse('main:groups_detail', args=(obj.group.pk,))
except Group.DoesNotExist:
pass
return res
class JobTemplateSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
class Meta:
model = JobTemplate
fields = ('url', 'id', 'related', 'name', 'description', 'job_type', 'credential', 'project', 'inventory', 'created_by', 'creation_date')
fields = BASE_FIELDS + ('job_type', 'inventory', 'project', 'playbook',
'credential', 'use_sudo', 'forks', 'limit',
'verbosity', 'extra_vars')
def get_related(self, obj):
# FIXME: fill in once further defined. related resources, credential, project, inventory, etc
res = dict(
credential = reverse('main:credentials_detail', args=(obj.credential.pk,)),
project = reverse('main:projects_detail', args=(obj.project.pk,)),
res = super(JobTemplateSerializer, self).get_related(obj)
res.update(dict(
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
project = reverse('main:projects_detail', args=(obj.project.pk,)),
jobs = reverse('main:job_template_job_list', args=(obj.pk,)),
))
if obj.credential:
res['credential'] = reverse('main:credentials_detail', args=(obj.credential.pk,))
return res
class JobSerializer(BaseSerializer):
# add the URL and related resources
url = serializers.CharField(source='get_absolute_url', read_only=True)
related = serializers.SerializerMethodField('get_related')
passwords_needed_to_start = serializers.Field(source='get_passwords_needed_to_start')
class Meta:
model = Job
fields = ('url', 'id', 'related', 'name', 'description', 'job_type', 'credential', 'project', 'inventory', 'created_by', 'creation_date')
fields = BASE_FIELDS + ('job_template', 'job_type', 'inventory',
'project', 'playbook', 'credential',
'use_sudo', 'forks', 'limit', 'verbosity',
'extra_vars', 'status', 'result_stdout',
'result_traceback', 'passwords_needed_to_start')
def get_related(self, obj):
# FIXME: fill in once further defined. related resources, credential, project, inventory, etc
res = dict(
)
if obj.created_by:
res['created_by'] = reverse('main:users_detail', args=(obj.created_by.pk,))
res = super(JobSerializer, self).get_related(obj)
res.update(dict(
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
project = reverse('main:projects_detail', args=(obj.project.pk,)),
credential = reverse('main:credentials_detail', args=(obj.credential.pk,)),
job_events = reverse('main:job_job_event_list', args=(obj.pk,)),
#hosts = reverse('main:job_???', args=(obj.pk,)),
))
if obj.job_template:
res['job_template'] = reverse('main:job_template_detail', args=(obj.job_template.pk,))
return res
class JobHostSummarySerializer(BaseSerializer):
class Meta:
model = JobHostSummary
fields = ('id', 'url', 'job', 'host', 'changed', 'dark', 'failures',
'ok', 'processed', 'skipped', 'related')
def get_related(self, obj):
res = super(JobHostSummarySerializer, self).get_related(obj)
res['job'] = reverse('main:job_detail', args=(obj.job.pk,))
if obj.host:
res['host'] = reverse('main:hosts_detail', args=(obj.host.pk,))
return res
class JobEventSerializer(BaseSerializer):
class Meta:
model = JobEvent
fields = ('id', 'url', 'job', 'event', 'event_data', 'host', 'related')
def get_related(self, obj):
res = super(JobEventSerializer, self).get_related(obj)
res.update(dict(
job = reverse('main:job_detail', args=(obj.job.pk,)),
))
if obj.host:
res['host'] = reverse('main:hosts_detail', args=(obj.host.pk,))
return res

View File

@ -46,17 +46,6 @@ class BaseTestMixin(object):
if os.path.exists(project_dir):
shutil.rmtree(project_dir, True)
def make_user(self, username, password=None, super_user=False):
user = None
password = password or username
if super_user:
user = User.objects.create_superuser(username, "%s@example.com", password)
else:
user = User.objects.create_user(username, "%s@example.com", password)
self.assertTrue(user.auth_token)
self._user_passwords[user.username] = password
return user
@contextlib.contextmanager
def current_user(self, user_or_username, password=None):
try:
@ -71,6 +60,17 @@ class BaseTestMixin(object):
finally:
self._current_auth = previous_auth
def make_user(self, username, password=None, super_user=False):
user = None
password = password or username
if super_user:
user = User.objects.create_superuser(username, "%s@example.com", password)
else:
user = User.objects.create_user(username, "%s@example.com", password)
self.assertTrue(user.auth_token)
self._user_passwords[user.username] = password
return user
def make_organizations(self, created_by, count=1):
results = []
for x in range(0, count):
@ -80,35 +80,51 @@ class BaseTestMixin(object):
))
return results
def make_projects(self, created_by, count=1, playbook_content=''):
results = []
def make_project(self, name, description='', created_by=None,
playbook_content=''):
if not os.path.exists(settings.PROJECTS_ROOT):
os.makedirs(settings.PROJECTS_ROOT)
# Create temp project directory.
project_dir = tempfile.mkdtemp(dir=settings.PROJECTS_ROOT)
self._temp_project_dirs.append(project_dir)
# Create temp playbook in project (if playbook content is given).
if playbook_content:
handle, playbook_path = tempfile.mkstemp(suffix='.yml',
dir=project_dir)
test_playbook_file = os.fdopen(handle, 'w')
test_playbook_file.write(playbook_content)
test_playbook_file.close()
return Project.objects.create(
name=name, description=description, local_path=project_dir,
created_by=created_by,
#scm_type='git', default_playbook='foo.yml',
)
def make_projects(self, created_by, count=1, playbook_content=''):
results = []
for x in range(0, count):
self.object_ctr = self.object_ctr + 1
# Create temp project directory.
project_dir = tempfile.mkdtemp(dir=settings.PROJECTS_ROOT)
self._temp_project_dirs.append(project_dir)
# Create temp playbook in project (if playbook content is given).
if playbook_content:
handle, playbook_path = tempfile.mkstemp(suffix='.yml',
dir=project_dir)
test_playbook_file = os.fdopen(handle, 'w')
test_playbook_file.write(playbook_content)
test_playbook_file.close()
results.append(Project.objects.create(
name="proj%s-%s" % (x, self.object_ctr), description="proj%s" % x,
#scm_type='git', default_playbook='foo.yml',
local_path=project_dir, created_by=created_by
results.append(self.make_project(
name="proj%s-%s" % (x, self.object_ctr),
description="proj%s" % x,
created_by=created_by,
playbook_content=playbook_content,
))
return results
def check_pagination_and_size(self, data, desired_count, previous=None, next=None):
self.assertEquals(data['previous'], previous)
self.assertEquals(data['next'], next)
self.assertTrue('results' in data)
self.assertEqual(data['count'], desired_count)
self.assertEqual(data['previous'], previous)
self.assertEqual(data['next'], next)
def check_list_ids(self, data, queryset, check_order=False):
data_ids = [x['id'] for x in data['results']]
qs_ids = queryset.values_list('pk', flat=True)
if check_order:
self.assertEqual(data_ids, qs_ids)
else:
self.assertEqual(set(data_ids), set(qs_ids))
def setup_users(self, just_super_user=False):
# Create a user.
@ -191,7 +207,7 @@ class BaseTestMixin(object):
# TODO: this test helper function doesn't support pagination
data = self.get(collection_url, expect=200, auth=auth)
return [item['url'] for item in data['results']]
class BaseTest(BaseTestMixin, django.test.TestCase):
'''
Base class for unit tests.

View File

@ -24,7 +24,7 @@ from lib.main.tests.base import BaseTest
__all__ = ['JobTemplateTest', 'JobTest']
TEST_PLAYBOOK = '''- hosts: mygroup
TEST_PLAYBOOK = '''- hosts: all
gather_facts: false
tasks:
- name: woohoo
@ -34,106 +34,360 @@ TEST_PLAYBOOK = '''- hosts: mygroup
class BaseJobTest(BaseTest):
''''''
def get_other2_credentials(self):
return ('other2', 'other2')
def _create_inventory(self, name, organization, created_by,
groups_hosts_dict):
'''Helper method for creating inventory with groups and hosts.'''
inventory = organization.inventories.create(
name=name,
created_by=created_by,
)
for group_name, host_names in groups_hosts_dict.items():
group = inventory.groups.create(
name=group_name,
created_by=created_by,
)
for host_name in host_names:
host = inventory.hosts.create(
name=host_name,
created_by=created_by,
)
group.hosts.add(host)
return inventory
def get_nobody_credentials(self):
return ('nobody', 'nobody')
def populate(self):
# Here's a little story about the Ansible Bread Company, or ABC. They
# make machines that make bread - bakers, slicers, and packagers - and
# these machines are each controlled by a Linux boxes, which is in turn
# managed by Ansible Commander.
# Sue is the super user. You don't mess with Sue or you're toast. Ha.
self.user_sue = self.make_user('sue', super_user=True)
# There are three organizations in ABC using Ansible, since it's the
# best thing for dev ops automation since, well, sliced bread.
# Engineering - They design and build the machines.
self.org_eng = Organization.objects.create(
name='engineering',
created_by=self.user_sue,
)
# Support - They fix it when it's not working.
self.org_sup = Organization.objects.create(
name='support',
created_by=self.user_sue,
)
# Operations - They implement the production lines using the machines.
self.org_ops = Organization.objects.create(
name='operations',
created_by=self.user_sue,
)
# Alex is Sue's IT assistant who can also administer all of the
# organizations.
self.user_alex = self.make_user('alex')
self.org_eng.admins.add(self.user_alex)
self.org_sup.admins.add(self.user_alex)
self.org_ops.admins.add(self.user_alex)
# Bob is the head of engineering. He's an admin for engineering, but
# also a user within the operations organization (so he can see the
# results if things go wrong in production).
self.user_bob = self.make_user('bob')
self.org_eng.admins.add(self.user_bob)
self.org_ops.users.add(self.user_bob)
# Chuck is the lead engineer. He has full reign over engineering, but
# no other organizations.
self.user_chuck = self.make_user('chuck')
self.org_eng.admins.add(self.user_chuck)
# Doug is the other engineer working under Chuck. He can write
# playbooks and check them, but Chuck doesn't quite think he's ready to
# run them yet. Poor Doug.
self.user_doug = self.make_user('doug')
self.org_eng.users.add(self.user_doug)
# Eve is the head of support. She can also see what goes on in
# operations to help them troubleshoot problems.
self.user_eve = self.make_user('eve')
self.org_sup.admins.add(self.user_eve)
self.org_ops.users.add(self.user_eve)
# Frank is the other support guy.
self.user_frank = self.make_user('frank')
self.org_sup.users.add(self.user_frank)
# Greg is the head of operations.
self.user_greg = self.make_user('greg')
self.org_ops.admins.add(self.user_greg)
# Holly is an operations engineer.
self.user_holly = self.make_user('holly')
self.org_ops.users.add(self.user_holly)
# Iris is another operations engineer.
self.user_iris = self.make_user('iris')
self.org_ops.users.add(self.user_iris)
# Jim is the intern. He can login, but can't do anything quite yet
# except make everyone else fresh coffee.
self.user_jim = self.make_user('jim')
# There are three main projects, one each for the development, test and
# production branches of the playbook repository. All three orgs can
# use the production branch, support can use the production and testing
# branches, and operations can only use the production branch.
self.proj_dev = self.make_project('dev', 'development branch',
self.user_sue, TEST_PLAYBOOK)
self.org_eng.projects.add(self.proj_dev)
self.proj_test = self.make_project('test', 'testing branch',
self.user_sue, TEST_PLAYBOOK)
self.org_eng.projects.add(self.proj_test)
self.org_sup.projects.add(self.proj_test)
self.proj_prod = self.make_project('prod', 'production branch',
self.user_sue, TEST_PLAYBOOK)
self.org_eng.projects.add(self.proj_prod)
self.org_sup.projects.add(self.proj_prod)
self.org_ops.projects.add(self.proj_prod)
# Operations also has 2 additional projects specific to the east/west
# production environments.
self.proj_prod_east = self.make_project('prod-east',
'east production branch',
self.user_sue, TEST_PLAYBOOK)
self.org_ops.projects.add(self.proj_prod_east)
self.proj_prod_west = self.make_project('prod-west',
'west production branch',
self.user_sue, TEST_PLAYBOOK)
self.org_ops.projects.add(self.proj_prod_west)
# The engineering organization has a set of servers to use for
# development and testing (2 bakers, 1 slicer, 1 packager).
self.inv_eng = self._create_inventory(
name='engineering environment',
organization=self.org_eng,
created_by=self.user_sue,
groups_hosts_dict={
'bakers': ['eng-baker1', 'eng-baker2'],
'slicers': ['eng-slicer1'],
'packagers': ['eng-packager1'],
},
)
# The support organization has a set of servers to use for
# testing and reproducing problems from operations (1 baker, 1 slicer,
# 1 packager).
self.inv_sup = self._create_inventory(
name='support environment',
organization=self.org_sup,
created_by=self.user_sue,
groups_hosts_dict={
'bakers': ['sup-baker1'],
'slicers': ['sup-slicer1'],
'packagers': ['sup-packager1'],
},
)
# The operations organization manages multiple sets of servers for the
# east and west production facilities.
self.inv_ops_east = self._create_inventory(
name='east production environment',
organization=self.org_ops,
created_by=self.user_sue,
groups_hosts_dict={
'bakers': ['east-baker%d' % n for n in range(1, 4)],
'slicers': ['east-slicer%d' % n for n in range(1, 3)],
'packagers': ['east-packager%d' % n for n in range(1, 3)],
},
)
self.inv_ops_west = self._create_inventory(
name='west production environment',
organization=self.org_ops,
created_by=self.user_sue,
groups_hosts_dict={
'bakers': ['west-baker%d' % n for n in range(1, 6)],
'slicers': ['west-slicer%d' % n for n in range(1, 4)],
'packagers': ['west-packager%d' % n for n in range(1, 3)],
},
)
# Operations is divided into teams to work on the east/west servers.
# Greg and Holly work on east, Greg and iris work on west.
self.team_ops_east = self.org_ops.teams.create(
name='easterners',
created_by=self.user_sue,
)
self.team_ops_east.projects.add(self.proj_prod)
self.team_ops_east.projects.add(self.proj_prod_east)
self.team_ops_east.users.add(self.user_greg)
self.team_ops_east.users.add(self.user_holly)
self.team_ops_west = self.org_ops.teams.create(
name='westerners',
created_by=self.user_sue,
)
self.team_ops_west.projects.add(self.proj_prod)
self.team_ops_west.projects.add(self.proj_prod_west)
self.team_ops_west.users.add(self.user_greg)
self.team_ops_west.users.add(self.user_iris)
# Each user has his/her own set of credentials.
from lib.main.tests.tasks import (TEST_SSH_KEY_DATA,
TEST_SSH_KEY_DATA_LOCKED,
TEST_SSH_KEY_DATA_UNLOCK)
self.cred_bob = self.user_bob.credentials.create(
ssh_username='bob',
ssh_password='ASK',
created_by=self.user_sue,
)
self.cred_chuck = self.user_chuck.credentials.create(
ssh_username='chuck',
ssh_key_data=TEST_SSH_KEY_DATA,
created_by=self.user_sue,
)
self.cred_doug = self.user_doug.credentials.create(
ssh_username='doug',
ssh_password='doug doesn\'t mind his password being saved. this '
'is why we dont\'t let doug actually run jobs.',
created_by=self.user_sue,
)
self.cred_eve = self.user_eve.credentials.create(
ssh_username='eve',
ssh_password='ASK',
created_by=self.user_sue,
)
self.cred_frank = self.user_frank.credentials.create(
ssh_username='frank',
ssh_password='fr@nk the t@nk',
created_by=self.user_sue,
)
self.cred_greg = self.user_greg.credentials.create(
ssh_username='greg',
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
ssh_key_unlock='ASK',
created_by=self.user_sue,
)
self.cred_holly = self.user_holly.credentials.create(
ssh_username='holly',
ssh_password='holly rocks',
created_by=self.user_sue,
)
self.cred_iris = self.user_iris.credentials.create(
ssh_username='iris',
ssh_password='',
created_by=self.user_sue,
)
# Each operations team also has shared credentials they can use.
self.cred_ops_east = self.team_ops_east.credentials.create(
ssh_username='east',
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK,
created_by = self.user_sue,
)
self.cred_ops_west = self.team_ops_west.credentials.create(
ssh_username='west',
ssh_password='Heading270',
created_by = self.user_sue,
)
# FIXME: Define explicit permissions for tests.
# other django user is on the project team and can deploy
#self.permission1 = Permission.objects.create(
# inventory = self.inventory,
# project = self.project,
# team = self.team,
# permission_type = PERM_INVENTORY_DEPLOY,
# created_by = self.normal_django_user
#)
# individual permission granted to other2 user, can run check mode
#self.permission2 = Permission.objects.create(
# inventory = self.inventory,
# project = self.project,
# user = self.other2_django_user,
# permission_type = PERM_INVENTORY_CHECK,
# created_by = self.normal_django_user
#)
# Engineering has job templates to check/run the dev project onto
# their own inventory.
self.jt_eng_check = JobTemplate.objects.create(
name='eng-dev-check',
job_type='check',
inventory= self.inv_eng,
project=self.proj_dev,
playbook=self.proj_dev.available_playbooks[0],
created_by=self.user_sue,
)
self.jt_eng_run = JobTemplate.objects.create(
name='eng-dev-run',
job_type='run',
inventory= self.inv_eng,
project=self.proj_dev,
playbook=self.proj_dev.available_playbooks[0],
created_by=self.user_sue,
)
# Support has job templates to check/run the test project onto
# their own inventory.
self.jt_sup_check = JobTemplate.objects.create(
name='sup-test-check',
job_type='check',
inventory= self.inv_sup,
project=self.proj_test,
playbook=self.proj_test.available_playbooks[0],
created_by=self.user_sue,
)
self.jt_sup_run = JobTemplate.objects.create(
name='sup-test-run',
job_type='run',
inventory= self.inv_sup,
project=self.proj_test,
playbook=self.proj_test.available_playbooks[0],
created_by=self.user_sue,
)
# Operations has job templates to check/run the prod project onto
# both east and west inventories, by default using the team credential.
self.jt_ops_east_check = JobTemplate.objects.create(
name='ops-east-prod-check',
job_type='check',
inventory= self.inv_ops_east,
project=self.proj_prod,
playbook=self.proj_prod.available_playbooks[0],
credential=self.cred_ops_east,
created_by=self.user_sue,
)
self.jt_ops_east_run = JobTemplate.objects.create(
name='ops-east-prod-run',
job_type='run',
inventory= self.inv_ops_east,
project=self.proj_prod,
playbook=self.proj_prod.available_playbooks[0],
credential=self.cred_ops_east,
created_by=self.user_sue,
)
self.jt_ops_west_check = JobTemplate.objects.create(
name='ops-west-prod-check',
job_type='check',
inventory= self.inv_ops_west,
project=self.proj_prod,
playbook=self.proj_prod.available_playbooks[0],
credential=self.cred_ops_west,
created_by=self.user_sue,
)
self.jt_ops_west_run = JobTemplate.objects.create(
name='ops-west-prod-run',
job_type='run',
inventory= self.inv_ops_west,
project=self.proj_prod,
playbook=self.proj_prod.available_playbooks[0],
credential=self.cred_ops_west,
created_by=self.user_sue,
)
def setUp(self):
super(BaseJobTest, self).setUp()
# Users
self.setup_users()
self.other2_django_user = self.make_user('other2', 'other2')
self.nobody_django_user = self.make_user('nobody', 'nobody')
# Organization
self.organization = Organization.objects.create(
name='engineering',
created_by=self.normal_django_user,
)
self.organization.admins.add(self.normal_django_user)
self.organization.users.add(self.normal_django_user)
self.organization.users.add(self.other_django_user)
self.organization.users.add(self.other2_django_user)
# Team
self.team = self.organization.teams.create(
name='Tigger',
created_by=self.normal_django_user,
)
self.team.users.add(self.other_django_user)
self.team.users.add(self.other2_django_user)
# Project
self.project = self.make_projects(self.normal_django_user, 1,
playbook_content=TEST_PLAYBOOK)[0]
self.organization.projects.add(self.project)
# Inventory
self.inventory = self.organization.inventories.create(
name = 'prod',
created_by = self.normal_django_user,
)
self.group_a = self.inventory.groups.create(
name = 'group1',
created_by = self.normal_django_user
)
self.host_a = self.inventory.hosts.create(
name = '127.0.0.1',
created_by = self.normal_django_user
)
self.host_b = self.inventory.hosts.create(
name = '127.0.0.2',
created_by = self.normal_django_user
)
self.group_a.hosts.add(self.host_a)
self.group_a.hosts.add(self.host_b)
# Credentials
self.user_credential = self.other_django_user.credentials.create(
ssh_key_data = 'xxx',
created_by = self.normal_django_user,
)
self.team_credential = self.team.credentials.create(
ssh_key_data = 'xxx',
created_by = self.normal_django_user,
)
# other django user is on the project team and can deploy
self.permission1 = Permission.objects.create(
inventory = self.inventory,
project = self.project,
team = self.team,
permission_type = PERM_INVENTORY_DEPLOY,
created_by = self.normal_django_user
)
# individual permission granted to other2 user, can run check mode
self.permission2 = Permission.objects.create(
inventory = self.inventory,
project = self.project,
user = self.other2_django_user,
permission_type = PERM_INVENTORY_CHECK,
created_by = self.normal_django_user
)
self.job_template1 = JobTemplate.objects.create(
name = 'job-run',
job_type = 'run',
inventory = self.inventory,
credential = self.user_credential,
project = self.project,
created_by = self.normal_django_user,
)
self.job_template2 = JobTemplate.objects.create(
name = 'job-check',
job_type = 'check',
inventory = self.inventory,
credential = self.team_credential,
project = self.project,
created_by = self.normal_django_user,
)
self.populate()
class JobTemplateTest(BaseJobTest):
@ -141,16 +395,58 @@ class JobTemplateTest(BaseJobTest):
def setUp(self):
super(JobTemplateTest, self).setUp()
def test_job_template_list(self):
url = reverse('main:job_templates_list')
response = self.get(url, expect=401)
with self.current_user(self.normal_django_user):
response = self.get(url, expect=200)
self.assertTrue(response['count'], JobTemplate.objects.count())
def test_get_job_template_list(self):
url = reverse('main:job_template_list')
# FIXME: Test that user can only see job templates from own organization.
# no credentials == 401
self.options(url, expect=401)
self.head(url, expect=401)
self.get(url, expect=401)
# wrong credentials == 401
with self.current_user('invalid', 'password'):
self.options(url, expect=401)
self.head(url, expect=401)
self.get(url, expect=401)
# 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)
# FIXME: Check individual job template result.
# 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
# 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],
)
#self.check_pagination_and_size(response, qs.count())
#self.check_list_ids(response, qs)
def test_post_job_template_list(self):
url = reverse('main:job_template_list')
return # FIXME
# org admin can add job template
data = dict(
name = 'job-foo',
@ -158,10 +454,11 @@ class JobTemplateTest(BaseJobTest):
inventory = self.inventory.pk,
project = self.project.pk,
job_type = PERM_INVENTORY_DEPLOY,
playbook = self.project.available_playbooks[0],
)
with self.current_user(self.normal_django_user):
response = self.post(url, data, expect=201)
detail_url = reverse('main:job_templates_detail',
detail_url = reverse('main:job_template_detail',
args=(response['id'],))
self.assertEquals(response['url'], detail_url)
@ -192,31 +489,62 @@ class JobTemplateTest(BaseJobTest):
data['job_type'] = PERM_INVENTORY_DEPLOY
response = self.post(url, data, expect=403)
def test_job_template_detail(self):
def test_get_job_template_detail(self):
return # FIXME
url = reverse('main:job_template_detail', args=(self.job_template1.pk,))
# verify we can also get the job template record
got = self.get(url, expect=200, auth=self.get_other2_credentials())
self.failUnlessEqual(got['url'], '/api/v1/job_templates/6/')
with self.current_user(self.other2_django_user):
self.options(url)
self.head(url)
response = self.get(url)
self.assertEqual(response['url'], url)
# TODO: add more tests that show
# the method used to START a JobTemplate follow the exact same permissions as those to create it ...
# and that jobs come back nicely serialized with related resources and so on ...
# that we can drill all the way down and can get at host failure lists, etc ...
def test_put_job_template_detail(self):
pass
def test_get_job_template_job_list(self):
pass
def test_post_job_template_job_list(self):
pass
class JobTest(BaseJobTest):
def setUp(self):
super(JobTest, self).setUp()
def test_mainline(self):
def test_get_job_list(self):
pass
def test_post_job_list(self):
pass
def test_get_job_detail(self):
pass
def test_put_job_detail(self):
pass
def test_post_job_detail(self):
pass
def test_get_job_host_list(self):
pass
def test_get_job_job_event_list(self):
pass
def _test_mainline(self):
url = reverse('main:job_list')
return # FIXME
# job templates
data = self.get('/api/v1/job_templates/', expect=401)
data = self.get('/api/v1/job_templates/', expect=200, auth=self.get_normal_credentials())

View File

@ -82,22 +82,30 @@ class OrganizationsTest(BaseTest):
with self.current_user(self.super_django_user):
self.options(url, expect=200)
self.head(url, expect=200)
data = self.get(url, expect=200)
self.check_pagination_and_size(data, 10, previous=None, next=None)
[self.assertTrue(key in data['results'][0]) for key in ['name', 'description', 'url', 'creation_date', 'id' ]]
response = self.get(url, expect=200)
self.check_pagination_and_size(response, 10, previous=None, next=None)
self.assertEqual(len(response['results']),
Organization.objects.count())
for field in ['id', 'url', 'name', 'description', 'creation_date']:
self.assertTrue(field in response['results'][0],
'field %s not in result' % field)
# check that the related URL functionality works
related = data['results'][0]['related']
related = response['results'][0]['related']
for x in [ 'audit_trail', 'projects', 'users', 'admins', 'tags' ]:
self.assertTrue(x in related and related[x].endswith("/%s/" % x), "looking for %s in related" % x)
# normal credentials == 200, get only organizations that I am actually added to (there are 2)
data = self.get(self.collection(), expect=200, auth=self.get_normal_credentials())
self.check_pagination_and_size(data, 2, previous=None, next=None)
# normal credentials == 200, get only organizations of which user is a member
with self.current_user(self.normal_django_user):
self.options(url, expect=200)
self.head(url, expect=200)
response = self.get(url, expect=200)
self.check_pagination_and_size(response, 2, previous=None, next=None)
# no admin rights? get empty list
data = self.get(self.collection(), expect=200, auth=self.get_other_credentials())
self.check_pagination_and_size(data, 0, previous=None, next=None)
with self.current_user(self.other_django_user):
response = self.get(url, expect=200)
self.check_pagination_and_size(response, 0, previous=None, next=None)
def test_get_item(self):

View File

@ -79,7 +79,7 @@ hosts_urls = patterns('lib.main.views',
url(r'^$', 'hosts_list'),
url(r'^(?P<pk>[0-9]+)/$', 'hosts_detail'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'hosts_variable_detail'),
url(r'^(?P<pk>[0-9]+)/job_events/', 'hosts_job_events_list'),
url(r'^(?P<pk>[0-9]+)/job_events/', 'host_job_event_list'),
)
groups_urls = patterns('lib.main.views',
@ -106,24 +106,25 @@ permissions_urls = patterns('lib.main.views',
)
job_templates_urls = patterns('lib.main.views',
url(r'^$', 'job_templates_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_templates_detail'),
url(r'^(?P<pk>[0-9]+)/start$', 'job_templates_start'),
url(r'^$', 'job_template_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_template_detail'),
url(r'^(?P<pk>[0-9]+)/jobs/$', 'job_template_job_list'),
)
jobs_urls = patterns('lib.main.views',
url(r'^$', 'jobs_list'),
url(r'^(?P<pk>[0-9]+)/$', 'jobs_detail'),
url(r'^(?P<pk>[0-9]+)/hosts$', 'jobs_hosts_list'),
url(r'^(?P<pk>[0-9]+)/successful_hosts$', 'jobs_successful_hosts_list'),
url(r'^(?P<pk>[0-9]+)/changed_hosts$', 'jobs_changed_hosts_list'),
url(r'^(?P<pk>[0-9]+)/failed_hosts$', 'jobs_failed_hosts_list'),
url(r'^(?P<pk>[0-9]+)/unreachable_hosts$', 'jobs_unreachable_hosts_list'),
url(r'^$', 'job_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_detail'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_hosts_list'),
url(r'^(?P<pk>[0-9]+)/successful_hosts/$', 'jobs_successful_hosts_list'),
url(r'^(?P<pk>[0-9]+)/changed_hosts/$', 'jobs_changed_hosts_list'),
url(r'^(?P<pk>[0-9]+)/failed_hosts/$', 'jobs_failed_hosts_list'),
url(r'^(?P<pk>[0-9]+)/unreachable_hosts/$', 'jobs_unreachable_hosts_list'),
url(r'^(?P<pk>[0-9]+)/job_events/$', 'job_job_event_list'),
)
job_events_urls = patterns('lib.main.views',
url(r'^$', 'job_events_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_events_detail'),
url(r'^$', 'job_event_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_event_detail'),
)
tags_urls = patterns('lib.main.views',

View File

@ -16,6 +16,7 @@
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
from lib.main.models import *
from django.contrib.auth.models import User
from lib.main.serializers import *
@ -36,6 +37,7 @@ import re
import sys
import json as python_json
from base_views import *
from lib.main.access import *
class ApiRootView(APIView):
'''
@ -77,8 +79,8 @@ class ApiV1RootView(APIView):
inventory = reverse('main:inventory_list'),
groups = reverse('main:groups_list'),
hosts = reverse('main:hosts_list'),
job_templates = reverse('main:job_templates_list'),
jobs = reverse('main:jobs_list'),
job_templates = reverse('main:job_template_list'),
jobs = reverse('main:job_list'),
authtoken = reverse('main:auth_token_view'),
me = reverse('main:users_me_list'),
)
@ -314,7 +316,8 @@ class TeamsPermissionsList(BaseSubList):
def _get_queryset(self):
team = Team.objects.get(pk=self.kwargs['pk'])
base = Permission.objects.filter(team = team)
if Team.can_user_administrate(self.request.user, team, None):
#if Team.can_user_administrate(self.request.user, team, None):
if check_user_access(self.request.user, Team, 'change', team, None):
return base
elif team.users.filter(pk=self.request.user.pk).count() > 0:
return base
@ -358,7 +361,8 @@ class TeamsCredentialsList(BaseSubList):
def _get_queryset(self):
team = Team.objects.get(pk=self.kwargs['pk'])
if not Team.can_user_administrate(self.request.user, team, None):
#if not Team.can_user_administrate(self.request.user, team, None):
if not check_user_access(self.request.user, Team, 'change', team, None):
if not (self.request.user.is_superuser or self.request.user in team.users.all()):
raise PermissionDenied()
project_credentials = Credential.objects.filter(
@ -476,7 +480,8 @@ class UsersTeamsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
return Team.objects.filter(users__in = [ user ])
@ -493,7 +498,8 @@ class UsersPermissionsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
return Permission.objects.filter(user=user)
@ -509,7 +515,8 @@ class UsersProjectsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
teams = user.teams.all()
return Project.objects.filter(teams__in = teams)
@ -527,7 +534,8 @@ class UsersCredentialsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
project_credentials = Credential.objects.filter(
team__users__in = [ user ]
@ -546,7 +554,8 @@ class UsersOrganizationsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
return Organization.objects.filter(users__in = [ user ])
@ -562,7 +571,8 @@ class UsersAdminOrganizationsList(BaseSubList):
def _get_queryset(self):
user = User.objects.get(pk=self.kwargs['pk'])
if not UserHelper.can_user_administrate(self.request.user, user, None):
#if not UserHelper.can_user_administrate(self.request.user, user, None):
if not check_user_access(self.request.user, User, 'change', user, None):
raise PermissionDenied()
return Organization.objects.filter(admins__in = [ user ])
@ -876,7 +886,7 @@ class VariableDetail(BaseDetail):
def put(self, request, *args, **kwargs):
raise PermissionDenied()
class JobTemplatesList(BaseList):
class JobTemplateList(BaseList):
model = JobTemplate
serializer_class = JobTemplateSerializer
@ -884,33 +894,36 @@ class JobTemplatesList(BaseList):
filter_fields = ('name',)
def _get_queryset(self):
'''
I can see job templates when I am a superuser, or I am an admin of the 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 JobTemplate.
'''
base = JobTemplate.objects
if self.request.user.is_superuser:
return base.all()
return base.filter(
project__organizations__admins__in = [ self.request.user ]
).distinct() | base.filter(
project__teams__users__in = [ self.request.user ]
).distinct()
return get_user_queryset(self.request.user, self.model)
class JobTemplatesDetail(BaseDetail):
class JobTemplateDetail(BaseDetail):
model = JobTemplate
serializer_class = JobTemplateSerializer
permission_classes = (CustomRbac,)
class JobTemplateJobList(BaseSubList):
model = Job
serializer_class = JobSerializer
permission_classes = (CustomRbac,)
# to allow the sub-aspect listing
parent_model = JobTemplate
relationship = 'jobs'
# to allow posting to this resource to create resources
postable = True
# FIXME: go back and add these to other SubLists
inject_primary_key_on_post_as = 'job_template'
severable = False
#filter_fields = ('name',)
def _get_queryset(self):
return self.model.objects.all() # FIXME
# FIxME: Verify read permission on the job template.
job_template = get_object_or_404(JobTemplate, pk=self.kwargs['pk'])
return job_template.jobs
class JobTemplatesStart(BaseDetail):
pass
class JobsList(BaseList):
class JobList(BaseList):
model = Job
serializer_class = JobSerializer
@ -919,10 +932,16 @@ class JobsList(BaseList):
def _get_queryset(self):
return self.model.objects.all() # FIXME
class JobsDetail(BaseDetail):
pass
class JobDetail(BaseDetail):
class JobsHostsList(BaseSubList):
model = Job
serializer_class = JobSerializer
permission_classes = (CustomRbac,)
def post(self, request, *args, **kwargs):
pass # FIXME
class JobHostsList(BaseSubList):
pass
class JobsSuccessfulHostsList(BaseSubList):
@ -937,14 +956,50 @@ class JobsFailedHostsList(BaseSubList):
class JobsUnreachableHostsList(BaseSubList):
pass
class JobEventsList(BaseList):
pass
class JobEventList(BaseList):
class JobEventsDetail(BaseDetail):
pass
model = JobEvent
serializer_class = JobEventSerializer
permission_classes = (CustomRbac,)
class HostsJobEventsList(BaseSubList):
pass
def _get_queryset(self):
return self.model.objects.all() # FIXME
class JobEventDetail(BaseDetail):
model = JobEvent
serializer_class = JobEventSerializer
permission_classes = (CustomRbac,)
class JobJobEventList(BaseSubList):
model = JobEvent
serializer_class = JobEventSerializer
permission_classes = (CustomRbac,)
parent_model = Job
relationship = 'job_events'
postable = False
severable = False
def _get_queryset(self):
job = get_object_or_404(Job, pk=self.kwargs['pk'])
# FIXME: Verify read permission on the job.
return job.job_events
class HostJobEventList(BaseSubList):
model = JobEvent
serializer_class = JobEventSerializer
permission_classes = (CustomRbac,)
parent_model = Host
relationship = 'job_events'
postable = False
severable = False
def _get_queryset(self):
host = get_object_or_404(Host, pk=self.kwargs['pk'])
# FIXME: Verify read permission on the host.
return host.job_events
# Create view functions for all of the class-based views to simplify inclusion

View File

@ -39,6 +39,7 @@ MANAGERS = ADMINS
REST_FRAMEWORK = {
'FILTER_BACKEND': 'lib.main.custom_filters.CustomFilterBackend',
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'lib.main.serializers.PaginationSerializer',
'PAGINATE_BY': 25,
'PAGINATE_BY_PARAM': 'page_size',
'DEFAULT_AUTHENTICATION_CLASSES': (
@ -236,5 +237,10 @@ LOGGING = {
# Comment the line below to show lots of permissions logging.
'propagate': False,
},
'lib.main.access': {
'handlers': ['null'],
# Comment the line below to show lots of permissions logging.
'propagate': False,
},
}
}

View File

@ -71,7 +71,7 @@ $(function() {
// Make linkes from relative URLs to resources.
$('span.str').each(function() {
var s = $(this).html();
if (s.match(/^\"\/.+\/\"$/)) {
if (s.match(/^\"\/.+\/\"$/) || s.match(/^\"\/.+\/\?.*\"$/)) {
$(this).html('"<a href=' + s + '>' + s.replace(/\"/g, '') + '</a>"');
}
});