diff --git a/lib/main/access.py b/lib/main/access.py new file mode 100644 index 0000000000..450dfcdfc6 --- /dev/null +++ b/lib/main/access.py @@ -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 = { + # : [, ...], + # ... +} + +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) diff --git a/lib/main/base_views.py b/lib/main/base_views.py index 3c41b0c849..1425f7cd85 100644 --- a/lib/main/base_views.py +++ b/lib/main/base_views.py @@ -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)) diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index b6af4dac1f..9993400c4f 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -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()) diff --git a/lib/main/rbac.py b/lib/main/rbac.py index 047cf6db06..ef8a2cba93 100644 --- a/lib/main/rbac.py +++ b/lib/main/rbac.py @@ -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): diff --git a/lib/main/serializers.py b/lib/main/serializers.py index dbe331a427..0d650ca094 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -14,33 +14,91 @@ # You should have received a copy of the GNU General Public License # along with Ansible Commander. If not, see . +# 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 diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py index 34a6c05c94..2c8fd094b1 100644 --- a/lib/main/tests/base.py +++ b/lib/main/tests/base.py @@ -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. diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py index b443213551..5844d03b4c 100644 --- a/lib/main/tests/jobs.py +++ b/lib/main/tests/jobs.py @@ -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()) diff --git a/lib/main/tests/organizations.py b/lib/main/tests/organizations.py index 11388dbf25..dc899c4070 100644 --- a/lib/main/tests/organizations.py +++ b/lib/main/tests/organizations.py @@ -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): diff --git a/lib/main/urls.py b/lib/main/urls.py index 6703b39eef..fe2a030efd 100644 --- a/lib/main/urls.py +++ b/lib/main/urls.py @@ -79,7 +79,7 @@ hosts_urls = patterns('lib.main.views', url(r'^$', 'hosts_list'), url(r'^(?P[0-9]+)/$', 'hosts_detail'), url(r'^(?P[0-9]+)/variable_data/$', 'hosts_variable_detail'), - url(r'^(?P[0-9]+)/job_events/', 'hosts_job_events_list'), + url(r'^(?P[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[0-9]+)/$', 'job_templates_detail'), - url(r'^(?P[0-9]+)/start$', 'job_templates_start'), + url(r'^$', 'job_template_list'), + url(r'^(?P[0-9]+)/$', 'job_template_detail'), + url(r'^(?P[0-9]+)/jobs/$', 'job_template_job_list'), ) jobs_urls = patterns('lib.main.views', - url(r'^$', 'jobs_list'), - url(r'^(?P[0-9]+)/$', 'jobs_detail'), - url(r'^(?P[0-9]+)/hosts$', 'jobs_hosts_list'), - url(r'^(?P[0-9]+)/successful_hosts$', 'jobs_successful_hosts_list'), - url(r'^(?P[0-9]+)/changed_hosts$', 'jobs_changed_hosts_list'), - url(r'^(?P[0-9]+)/failed_hosts$', 'jobs_failed_hosts_list'), - url(r'^(?P[0-9]+)/unreachable_hosts$', 'jobs_unreachable_hosts_list'), + url(r'^$', 'job_list'), + url(r'^(?P[0-9]+)/$', 'job_detail'), + url(r'^(?P[0-9]+)/hosts/$', 'job_hosts_list'), + url(r'^(?P[0-9]+)/successful_hosts/$', 'jobs_successful_hosts_list'), + url(r'^(?P[0-9]+)/changed_hosts/$', 'jobs_changed_hosts_list'), + url(r'^(?P[0-9]+)/failed_hosts/$', 'jobs_failed_hosts_list'), + url(r'^(?P[0-9]+)/unreachable_hosts/$', 'jobs_unreachable_hosts_list'), + url(r'^(?P[0-9]+)/job_events/$', 'job_job_event_list'), ) job_events_urls = patterns('lib.main.views', - url(r'^$', 'job_events_list'), - url(r'^(?P[0-9]+)/$', 'job_events_detail'), + url(r'^$', 'job_event_list'), + url(r'^(?P[0-9]+)/$', 'job_event_detail'), ) tags_urls = patterns('lib.main.views', diff --git a/lib/main/views.py b/lib/main/views.py index bec014d48d..dd3d59dd8a 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -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 diff --git a/lib/settings/defaults.py b/lib/settings/defaults.py index 6ce341fc4c..7cc955492d 100644 --- a/lib/settings/defaults.py +++ b/lib/settings/defaults.py @@ -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, + }, } } diff --git a/lib/templates/rest_framework/api.html b/lib/templates/rest_framework/api.html index 56a2222de6..61057cdc14 100644 --- a/lib/templates/rest_framework/api.html +++ b/lib/templates/rest_framework/api.html @@ -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('"' + s.replace(/\"/g, '') + '"'); } });