mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Renamed some API files/classes to mimic REST framework names, moved queryset filtering for permissions alongside other permissions/access checks, cleaned up base views to handle get_queryset based on class attributes, cleaned up post to sublist to create/attach/unattach.
This commit is contained in:
parent
401317cf41
commit
a6c767907e
@ -1,15 +1,18 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# Django
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from awx import MODE
|
||||
# AWX
|
||||
from awx.main.models import *
|
||||
from awx.main.licenses import LicenseReader
|
||||
|
||||
@ -63,6 +66,12 @@ def check_user_access(user, model_class, action, *args, **kwargs):
|
||||
return False
|
||||
|
||||
class BaseAccess(object):
|
||||
'''
|
||||
Base class for checking user access to a given model. Subclasses should
|
||||
define the model attribute, override the get_queryset method to return only
|
||||
the instances the user should be able to view, and override/define can_*
|
||||
methods to verify a user's permission to perform a particular action.
|
||||
'''
|
||||
|
||||
model = None
|
||||
|
||||
@ -102,8 +111,7 @@ class BaseAccess(object):
|
||||
return self.can_change(obj, None)
|
||||
else:
|
||||
return bool(self.can_change(obj, None) and
|
||||
check_user_access(self.user, type(sub_obj), 'read',
|
||||
sub_obj))
|
||||
self.user.can_access(type(sub_obj), 'read', sub_obj))
|
||||
|
||||
def can_unattach(self, obj, sub_obj, relationship):
|
||||
return self.can_change(obj, None)
|
||||
@ -120,7 +128,7 @@ class UserAccess(BaseAccess):
|
||||
return self.model.objects.filter(is_active=True).filter(
|
||||
Q(pk=self.user.pk) |
|
||||
Q(organizations__in=self.user.admin_of_organizations.all()) |
|
||||
Q(teams__in=self.request.user.teams.all())
|
||||
Q(teams__in=self.user.teams.all())
|
||||
).distinct()
|
||||
|
||||
def can_read(self, obj):
|
||||
@ -158,6 +166,14 @@ class OrganizationAccess(BaseAccess):
|
||||
|
||||
model = Organization
|
||||
|
||||
def get_queryset(self):
|
||||
# I can see organizations when I am a superuser, or I am an admin or
|
||||
# user in that organization.
|
||||
qs = self.model.objects.distinct()
|
||||
if self.user.is_superuser:
|
||||
return qs
|
||||
return qs.filter(Q(admins__in=[self.user]) | Q(users__in=[self.user]))
|
||||
|
||||
def can_read(self, obj):
|
||||
return bool(self.can_change(obj, None) or
|
||||
self.user in obj.users.all())
|
||||
@ -174,6 +190,23 @@ class InventoryAccess(BaseAccess):
|
||||
|
||||
model = Inventory
|
||||
|
||||
def get_queryset(self):
|
||||
# I can see inventory when I'm a superuser, an org admin of the
|
||||
# inventory, or I have permissions on it.
|
||||
base = Inventory.objects.distinct()
|
||||
if self.user.is_superuser:
|
||||
return base.all()
|
||||
admin_of = base.filter(organization__admins__in = [ self.user ]).distinct()
|
||||
has_user_perms = base.filter(
|
||||
permissions__user__in = [ self.user ],
|
||||
permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
has_team_perms = base.filter(
|
||||
permissions__team__in = self.user.teams.all(),
|
||||
permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
return admin_of | has_user_perms | has_team_perms
|
||||
|
||||
def _has_permission_types(self, obj, allowed):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
@ -241,7 +274,7 @@ class InventoryAccess(BaseAccess):
|
||||
''' whether you can add sub_obj to obj using the relationship type in a subobject view '''
|
||||
#if not sub_obj.can_user_read(user, sub_obj):
|
||||
if sub_obj and not skip_sub_obj_read_check:
|
||||
if not check_user_access(self.user, type(sub_obj), 'read', sub_obj):
|
||||
if not self.user.can_access(type(sub_obj), 'read', sub_obj):
|
||||
return False
|
||||
return self._has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE)
|
||||
|
||||
@ -252,8 +285,29 @@ class HostAccess(BaseAccess):
|
||||
|
||||
model = Host
|
||||
|
||||
def get_queryset(self):
|
||||
'''
|
||||
I can see hosts when:
|
||||
I'm a superuser,
|
||||
or an organization admin of an inventory they are in
|
||||
or when I have allowing read permissions via a user or team on an inventory they are in
|
||||
'''
|
||||
base = self.model.objects
|
||||
if self.user.is_superuser:
|
||||
return base.all()
|
||||
admin_of = base.filter(inventory__organization__admins__in = [ self.user ]).distinct()
|
||||
has_user_perms = base.filter(
|
||||
inventory__permissions__user__in = [ self.user ],
|
||||
inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
has_team_perms = base.filter(
|
||||
inventory__permissions__team__in = self.user.teams.all(),
|
||||
inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
return admin_of | has_user_perms | has_team_perms
|
||||
|
||||
def can_read(self, obj):
|
||||
return check_user_access(self.user, Inventory, 'read', obj.inventory)
|
||||
return self.user.can_access(Inventory, 'read', obj.inventory)
|
||||
|
||||
def can_add(self, data):
|
||||
|
||||
@ -264,7 +318,7 @@ class HostAccess(BaseAccess):
|
||||
inventory = Inventory.objects.get(pk=data['inventory'])
|
||||
|
||||
# Checks for admin or change permission on inventory.
|
||||
permissions_ok = check_user_access(self.user, Inventory, 'change', inventory, None)
|
||||
permissions_ok = self.user.can_access(Inventory, 'change', inventory, None)
|
||||
if not permissions_ok:
|
||||
return False
|
||||
|
||||
@ -286,31 +340,82 @@ class HostAccess(BaseAccess):
|
||||
def can_change(self, obj, data):
|
||||
# Checks for admin or change permission on inventory, controls whether
|
||||
# the user can edit variable data.
|
||||
return check_user_access(self.user, Inventory, 'change', obj.inventory, None)
|
||||
return self.user.can_access(Inventory, 'change', obj.inventory, None)
|
||||
|
||||
class GroupAccess(BaseAccess):
|
||||
|
||||
model = Group
|
||||
|
||||
def get_queryset(self):
|
||||
'''
|
||||
I can see groups when:
|
||||
I'm a superuser,
|
||||
or an organization admin of an inventory they are in
|
||||
or when I have allowing read permissions via a user or team on an inventory they are in
|
||||
'''
|
||||
base = Group.objects
|
||||
if self.user.is_superuser:
|
||||
return base.distinct()
|
||||
admin_of = base.filter(inventory__organization__admins__in = [ self.user ]).distinct()
|
||||
has_user_perms = base.filter(
|
||||
inventory__permissions__user__in = [ self.user ],
|
||||
inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
has_team_perms = base.filter(
|
||||
inventory__permissions__team__in = self.user.teams.all(),
|
||||
inventory__permissions__permission_type__in = PERMISSION_TYPES_ALLOWING_INVENTORY_READ,
|
||||
).distinct()
|
||||
return admin_of | has_user_perms | has_team_perms
|
||||
|
||||
def can_read(self, obj):
|
||||
return check_user_access(self.user, Inventory, 'read', obj.inventory)
|
||||
return self.user.can_access(Inventory, 'read', obj.inventory)
|
||||
|
||||
def can_add(self, data):
|
||||
if not 'inventory' in data:
|
||||
return False
|
||||
inventory = Inventory.objects.get(pk=data['inventory'])
|
||||
# Checks for admin or change permission on inventory.
|
||||
return check_user_access(self.user, Inventory, 'change', inventory, None)
|
||||
return self.user.can_access(Inventory, 'change', inventory, None)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
# Checks for admin or change permission on inventory, controls whether
|
||||
# the user can attach subgroups or edit variable data.
|
||||
return check_user_access(self.user, Inventory, 'change', obj.inventory, None)
|
||||
return self.user.can_access(Inventory, 'change', obj.inventory, None)
|
||||
|
||||
def can_attach(self, obj, sub_obj, relationship, data,
|
||||
skip_sub_obj_read_check=False):
|
||||
if not self.can_change(obj, None):
|
||||
return False
|
||||
if sub_obj and not skip_sub_obj_read_check:
|
||||
if not self.user.can_access(type(sub_obj), 'read', sub_obj):
|
||||
return False
|
||||
|
||||
# Prevent group from being assigned as its own (grand)child.
|
||||
if type(obj) == type(sub_obj):
|
||||
parent_pks = set(obj.all_parents.values_list('pk', flat=True))
|
||||
parent_pks.add(obj.pk)
|
||||
child_pks = set(sub_obj.all_children.values_list('pk', flat=True))
|
||||
child_pks.add(sub_obj.pk)
|
||||
if parent_pks & child_pks:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class CredentialAccess(BaseAccess):
|
||||
|
||||
model = Credential
|
||||
|
||||
def get_queryset(self):
|
||||
# I can see credentials when:
|
||||
# - It's a user credential and it's my credential.
|
||||
# - It's a user credential and I'm an admin of an organization
|
||||
# -
|
||||
# FIXME
|
||||
qs = self.model.objects.distinct()
|
||||
if self.user.is_superuser:
|
||||
return qs
|
||||
return qs.filter(Q(user=self.user))
|
||||
|
||||
def can_read(self, obj):
|
||||
return self.can_change(obj, None)
|
||||
|
||||
@ -319,10 +424,10 @@ class CredentialAccess(BaseAccess):
|
||||
return True
|
||||
if 'user' in data:
|
||||
user_obj = User.objects.get(pk=data['user'])
|
||||
return check_user_access(self.user, User, 'change', user_obj, None)
|
||||
return self.user.can_access(User, 'change', user_obj, None)
|
||||
if 'team' in data:
|
||||
team_obj = Team.objects.get(pk=data['team'])
|
||||
return check_user_access(self.user, Team, 'change', team_obj, None)
|
||||
return self.user.can_access(Team, 'change', team_obj, None)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
if self.user.is_superuser:
|
||||
@ -347,6 +452,9 @@ class TeamAccess(BaseAccess):
|
||||
|
||||
model = Team
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.distinct() # FIXME
|
||||
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
@ -378,6 +486,22 @@ class ProjectAccess(BaseAccess):
|
||||
|
||||
model = Project
|
||||
|
||||
def get_queryset(self):
|
||||
# I can see projects when:
|
||||
# - I am a superuser
|
||||
# - I am an admin or user in that organization...
|
||||
# FIXME
|
||||
base = Project.objects.distinct()
|
||||
if self.user.is_superuser:
|
||||
return base.all()
|
||||
my_teams = Team.objects.filter(users__in = [ self.user])
|
||||
my_orgs = Organization.objects.filter(admins__in = [ self.user ])
|
||||
return base.filter(
|
||||
teams__in = my_teams
|
||||
).distinct() | base.filter(
|
||||
organizations__in = my_orgs
|
||||
).distinct()
|
||||
|
||||
def can_read(self, obj):
|
||||
if self.can_change(obj, None):
|
||||
return True
|
||||
@ -403,6 +527,9 @@ class PermissionAccess(BaseAccess):
|
||||
|
||||
model = Permission
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.distinct() # FIXME
|
||||
|
||||
def can_read(self, obj):
|
||||
# a permission can be seen by the assigned user or team
|
||||
# or anyone who can administrate that permission
|
||||
@ -524,6 +651,9 @@ class JobAccess(BaseAccess):
|
||||
|
||||
model = Job
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.distinct() # FIXME
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser and obj.status == 'new'
|
||||
|
||||
@ -537,10 +667,16 @@ class JobHostSummaryAccess(BaseAccess):
|
||||
|
||||
model = JobHostSummary
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.distinct() # FIXME
|
||||
|
||||
class JobEventAccess(BaseAccess):
|
||||
|
||||
model = JobEvent
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.distinct() # FIXME
|
||||
|
||||
register_access(User, UserAccess)
|
||||
register_access(Organization, OrganizationAccess)
|
||||
register_access(Inventory, InventoryAccess)
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework import authentication
|
||||
from rest_framework import exceptions
|
||||
|
||||
@ -6,66 +6,90 @@ import json
|
||||
|
||||
# Django
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import now
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import mixins
|
||||
from rest_framework import generics
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# AWX
|
||||
from awx.main.models import *
|
||||
from awx.main.serializers import *
|
||||
from awx.main.rbac import *
|
||||
from awx.main.access import *
|
||||
|
||||
# FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS
|
||||
|
||||
class BaseList(generics.ListCreateAPIView):
|
||||
|
||||
permission_classes = (CustomRbac,)
|
||||
class ListAPIView(generics.ListAPIView):
|
||||
# Base class for a read-only list view.
|
||||
|
||||
# Subclasses should define:
|
||||
# model = ModelClass
|
||||
# serializer_class = SerializerClass
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# FIXME: Should inherit from generics.ListAPIView if not postable.
|
||||
postable = getattr(self.__class__, 'postable', True)
|
||||
if not postable:
|
||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
return super(BaseList, self).post(request, *args, **kwargs)
|
||||
def get_queryset(self):
|
||||
return self.request.user.get_queryset(self.model)
|
||||
|
||||
# NOTE: Moved filtering from get_queryset into custom filter backend.
|
||||
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a list view that allows creating new objects.
|
||||
pass
|
||||
|
||||
class BaseSubList(BaseList):
|
||||
|
||||
''' used for subcollections with an overriden post '''
|
||||
class SubListAPIView(ListAPIView):
|
||||
# Base class for a read-only sublist view.
|
||||
|
||||
# Subclasses should define at least:
|
||||
# model = ModelClass
|
||||
# serializer_class = SerializerClass
|
||||
# parent_model = ModelClass
|
||||
# relationship = 'rel_name_from_parent_to_model'
|
||||
# And optionally:
|
||||
# postable = True/False
|
||||
# And optionally (user must have given access permission on parent object
|
||||
# to view sublist):
|
||||
# parent_access = 'admin'
|
||||
|
||||
def get_parent_object(self):
|
||||
parent_filter = {
|
||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
||||
}
|
||||
return get_object_or_404(self.parent_model, **parent_filter)
|
||||
|
||||
def check_parent_access(self, parent=None):
|
||||
parent = parent or self.get_parent_object()
|
||||
parent_access = getattr(self, 'parent_access', 'admin')
|
||||
if parent_access in ('read', 'delete'):
|
||||
args = (self.parent_model, parent_access, parent)
|
||||
else:
|
||||
args = (self.parent_model, parent_access, parent, None)
|
||||
if not self.request.user.can_access(*args):
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
qs = self.request.user.get_queryset(self.model).distinct()
|
||||
sublist_qs = getattr(parent, self.relationship).distinct()
|
||||
return qs & sublist_qs
|
||||
|
||||
class SubListCreateAPIView(SubListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects and
|
||||
# attaching/detaching them from the parent.
|
||||
|
||||
# In addition to SubListAPIView properties, subclasses may define:
|
||||
# inject_primary_key_on_post_as = 'field_on_model_referring_to_parent'
|
||||
# severable = True/False
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def create(self, request, *args, **kwargs):
|
||||
# If the object ID was not specified, it probably doesn't exist in the
|
||||
# DB yet. We want to see if we can create it. The URL may choose to
|
||||
# inject it's primary key into the object because we are posting to a
|
||||
# subcollection. Use all the normal access control mechanisms.
|
||||
|
||||
# decide whether to return 201 with data (new object) or 204 (just associated)
|
||||
created = False
|
||||
ser = None
|
||||
inject_primary_key = getattr(self, 'inject_primary_key_on_post_as', None)
|
||||
|
||||
postable = getattr(self.__class__, 'postable', False)
|
||||
if not postable:
|
||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
if inject_primary_key is None:
|
||||
# view didn't specify a way to get the pk from the URL, so not even trying
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST,
|
||||
data=dict(msg='object cannot be created'))
|
||||
|
||||
# Make a copy of the data provided (since it's readonly) in order to
|
||||
# inject additional data.
|
||||
@ -73,157 +97,100 @@ class BaseSubList(BaseList):
|
||||
data = request.DATA.dict()
|
||||
else:
|
||||
data = request.DATA
|
||||
parent_id = kwargs['pk']
|
||||
sub_id = data.get('id', None)
|
||||
main = self.__class__.parent_model.objects.get(pk=parent_id)
|
||||
severable = getattr(self.__class__, 'severable', True)
|
||||
|
||||
subs = None
|
||||
# add the key to the post data using the pk from the URL
|
||||
data[inject_primary_key] = kwargs['pk']
|
||||
|
||||
if sub_id:
|
||||
subs = self.__class__.model.objects.filter(pk=sub_id)
|
||||
else:
|
||||
if 'disassociate' in data:
|
||||
raise PermissionDenied() # ID is required to disassociate
|
||||
else:
|
||||
# attempt to deserialize the object
|
||||
serializer = self.serializer_class(data=data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors,
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# this is a little tricky and a little manual
|
||||
# the object ID was not specified, so it probably doesn't exist in the DB yet.
|
||||
# we want to see if we can create it. The URL may choose to inject it's primary key into the object
|
||||
# because we are posting to a subcollection. Use all the normal access control mechanisms.
|
||||
# Verify we have permission to add the object as given.
|
||||
if not request.user.can_access(self.model, 'add', serializer.init_data):
|
||||
raise PermissionDenied()
|
||||
|
||||
inject_primary_key = getattr(self.__class__, 'inject_primary_key_on_post_as', None)
|
||||
# save the object through the serializer, reload and returned the saved object deserialized
|
||||
obj = serializer.save()
|
||||
serializer = self.serializer_class(obj)
|
||||
|
||||
if inject_primary_key is not None:
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
# add the key to the post data using the pk from the URL
|
||||
data[inject_primary_key] = kwargs['pk']
|
||||
def attach(self, request, *args, **kwargs):
|
||||
created = False
|
||||
parent = self.get_parent_object()
|
||||
relationship = getattr(parent, self.relationship)
|
||||
sub_id = request.DATA.get('id', None)
|
||||
data = request.DATA
|
||||
|
||||
# attempt to deserialize the object
|
||||
ser = self.__class__.serializer_class(data=data)
|
||||
if not ser.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data=ser.errors)
|
||||
# Create the sub object if an ID is not provided.
|
||||
if not sub_id:
|
||||
response = self.create(request, *args, **kwargs)
|
||||
if response.status_code != status.HTTP_201_CREATED:
|
||||
return response
|
||||
sub_id = response.data['id']
|
||||
data = response.data
|
||||
created = True
|
||||
|
||||
if not check_user_access(request.user, self.model, 'add', ser.init_data):
|
||||
raise PermissionDenied()
|
||||
# Retrive the sub object (whether created or by ID).
|
||||
try:
|
||||
sub = self.model.objects.get(pk=sub_id)
|
||||
except self.model.DoesNotExist:
|
||||
data = dict(msg='Object with id %s cannot be found' % sub_id)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Verify we have permission to attach.
|
||||
if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created):
|
||||
raise PermissionDenied()
|
||||
|
||||
# save the object through the serializer, reload and returned the saved object deserialized
|
||||
obj = ser.save()
|
||||
ser = self.__class__.serializer_class(obj)
|
||||
|
||||
# now make sure we could have already attached the two together. If we could not have, raise an exception
|
||||
# such that the transaction does not commit.
|
||||
|
||||
if main == obj:
|
||||
# no attaching to yourself
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
if self.__class__.parent_model != User:
|
||||
|
||||
# FIXME: refactor into smaller functions
|
||||
|
||||
if obj.__class__ in [ User]:
|
||||
if self.__class__.parent_model == Organization:
|
||||
# user can't inject an organization because it's not part of the user
|
||||
# model so we have to cheat here. This may happen for other cases
|
||||
# where we are creating a user immediately on a subcollection
|
||||
# when that user does not already exist. Relations will work post-save.
|
||||
organization = Organization.objects.get(pk=data[inject_primary_key])
|
||||
if not request.user.is_superuser:
|
||||
if not organization.admins.filter(pk=request.user.pk).count() > 0:
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
if not check_user_access(request.user, type(obj), 'read', obj):
|
||||
raise PermissionDenied()
|
||||
# If we just created a new object, we may not yet be able to read it because it's not yet associated with its parent model.
|
||||
if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data, skip_sub_obj_read_check=True):
|
||||
raise PermissionDenied()
|
||||
|
||||
# FIXME: manual attachment code neccessary for users here, move this into the main code.
|
||||
# this is because users don't have FKs into what they are attaching. (also refactor)
|
||||
|
||||
if self.__class__.parent_model == Organization:
|
||||
organization = Organization.objects.get(pk=data[inject_primary_key])
|
||||
import awx.main.views
|
||||
if self.__class__ == awx.main.views.OrganizationUsersList:
|
||||
organization.users.add(obj)
|
||||
elif self.__class__ == awx.main.views.OrganizationAdminsList:
|
||||
organization.admins.add(obj)
|
||||
|
||||
else:
|
||||
if not check_user_access(request.user, type(obj), 'read', obj):
|
||||
raise PermissionDenied()
|
||||
# FIXME: should generalize this
|
||||
if not check_user_access(request.user, self.parent_model, 'attach', main, obj, self.relationship, data):
|
||||
raise PermissionDenied()
|
||||
|
||||
# why are we returning here?
|
||||
# return Response(status=status.HTTP_201_CREATED, data=ser.data)
|
||||
created = True
|
||||
subs = [ obj ]
|
||||
|
||||
else:
|
||||
|
||||
# view didn't specify a way to get the pk from the URL, so not even trying
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data=json.dumps(dict(msg='object cannot be created')))
|
||||
|
||||
# we didn't have to create the object, so this is just associating the two objects together now...
|
||||
# (or disassociating them)
|
||||
|
||||
if len(subs) != 1:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
sub = subs[0]
|
||||
relationship = getattr(main, self.__class__.relationship)
|
||||
|
||||
if not 'disassociate' in data:
|
||||
if not request.user.is_superuser:
|
||||
if type(main) != User:
|
||||
#if not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship, data):
|
||||
if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data):
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
#if not UserHelper.can_user_attach(request.user, main, sub, self.__class__.relationship, data):
|
||||
if not check_user_access(request.user, self.parent_model, 'attach', main, sub, self.relationship, data):
|
||||
raise PermissionDenied()
|
||||
|
||||
if sub not in relationship.all():
|
||||
relationship.add(sub)
|
||||
else:
|
||||
if not request.user.is_superuser:
|
||||
if type(main) != User:
|
||||
#if not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship):
|
||||
if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship):
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
#if not UserHelper.can_user_unattach(request.user, main, sub, self.__class__.relationship):
|
||||
if not check_user_access(request.user, self.parent_model, 'unattach', main, sub, self.relationship):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
if severable:
|
||||
relationship.remove(sub)
|
||||
else:
|
||||
# resource is just a ForeignKey, can't remove it from the set, just set it inactive
|
||||
sub.mark_inactive()
|
||||
|
||||
|
||||
# verify we didn't add anything to it's own children
|
||||
if type(main) == Group:
|
||||
all_children = main.get_all_children().all()
|
||||
if main in all_children:
|
||||
# no attaching to child objects (in the case of groups)
|
||||
raise PermissionDenied()
|
||||
# Attach the object to the collection.
|
||||
if sub not in relationship.all():
|
||||
relationship.add(sub)
|
||||
|
||||
if created:
|
||||
return Response(status=status.HTTP_201_CREATED, data=ser.data)
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def unattach(self, request, *args, **kwargs):
|
||||
sub_id = request.DATA.get('id', None)
|
||||
if not sub_id:
|
||||
data = dict(msg='"id" is required to disassociate')
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class BaseDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
parent = self.get_parent_object()
|
||||
severable = getattr(self, 'severable', True)
|
||||
relationship = getattr(parent, self.relationship)
|
||||
|
||||
try:
|
||||
sub = self.model.objects.get(pk=sub_id)
|
||||
except self.model.DoesNotExist:
|
||||
data = dict(msg='Object with id %s cannot be found' % sub_id)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship):
|
||||
raise PermissionDenied()
|
||||
|
||||
if severable:
|
||||
relationship.remove(sub)
|
||||
else:
|
||||
# resource is just a ForeignKey, can't remove it from the set, just set it inactive
|
||||
sub.mark_inactive()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'disassociate' in request.DATA:
|
||||
return self.unattach(request, *args, **kwargs)
|
||||
else:
|
||||
return self.attach(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveAPIView(generics.RetrieveAPIView):
|
||||
pass
|
||||
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
def pre_save(self, obj):
|
||||
if type(obj) not in [ User ]:
|
||||
@ -237,23 +204,18 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
raise Http404()
|
||||
if getattr(obj, 'is_active', True) == False:
|
||||
raise Http404()
|
||||
#if not request.user.is_superuser and not self.delete_permissions_check(request, obj):
|
||||
if not check_user_access(request.user, self.model, 'delete', obj):
|
||||
if not request.user.can_access(self.model, 'delete', obj):
|
||||
raise PermissionDenied()
|
||||
if isinstance(obj, PrimordialModel):
|
||||
if hasattr(obj, 'mark_inactive'):
|
||||
obj.mark_inactive()
|
||||
elif type(obj) == User:
|
||||
obj.username = "_deleted_%s_%s" % (now().isoformat(), obj.username)
|
||||
obj.is_active = False
|
||||
obj.save()
|
||||
else:
|
||||
raise Exception("InternalError: destroy() not implemented yet for %s" % obj)
|
||||
return HttpResponse(status=204)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
self.put_filter(request, *args, **kwargs)
|
||||
return super(BaseDetail, self).put(request, *args, **kwargs)
|
||||
def update(self, request, *args, **kwargs):
|
||||
self.update_filter(request, *args, **kwargs)
|
||||
return super(RetrieveUpdateDestroyAPIView, self).update(request, *args, **kwargs)
|
||||
|
||||
def put_filter(self, request, *args, **kwargs):
|
||||
''' scrub any fields the user cannot/should not put, based on user context. This runs after read-only serialization filtering '''
|
||||
def update_filter(self, request, *args, **kwargs):
|
||||
''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
|
||||
pass
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
'''
|
||||
Compability library for support of both Django 1.4.x and Django 1.5.x.
|
||||
'''
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
class CustomFilterBackend(object):
|
||||
class DefaultFilterBackend(BaseFilterBackend):
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
|
||||
@ -33,6 +33,10 @@ from djcelery.models import TaskMeta
|
||||
# Django-REST-Framework
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
__all__ = ['Organization', 'Team', 'Project', 'Credential', 'Inventory',
|
||||
'Host', 'Group', 'Permission', 'JobTemplate', 'Job',
|
||||
'JobHostSummary', 'JobEvent']
|
||||
|
||||
# TODO: reporting model TBD
|
||||
|
||||
PERM_INVENTORY_ADMIN = 'admin'
|
||||
@ -1213,6 +1217,21 @@ class JobEvent(models.Model):
|
||||
|
||||
# TODO: reporting (MPD)
|
||||
|
||||
# Add mark_inactive method to User model.
|
||||
def user_mark_inactive(user, save=True):
|
||||
'''Use instead of delete to rename and mark users inactive.'''
|
||||
if user.is_active:
|
||||
user.username = "_deleted_%s_%s" % (now().isoformat(), user.username)
|
||||
user.is_active = False
|
||||
if save:
|
||||
user.save()
|
||||
User.add_to_class('mark_inactive', user_mark_inactive)
|
||||
|
||||
# Add custom methods to User model for permissions checks.
|
||||
from awx.main.access import *
|
||||
User.add_to_class('get_queryset', get_user_queryset)
|
||||
User.add_to_class('can_access', check_user_access)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_auth_token_for_user(sender, **kwargs):
|
||||
instance = kwargs.get('instance', None)
|
||||
|
||||
@ -1,26 +1,38 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import logging
|
||||
|
||||
# Django
|
||||
from django.http import Http404
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import permissions
|
||||
|
||||
# AWX
|
||||
from awx.main.access import *
|
||||
from awx.main.models import *
|
||||
|
||||
logger = logging.getLogger('awx.main.rbac')
|
||||
logger = logging.getLogger('awx.main.permissions')
|
||||
|
||||
# FIXME: this will probably need to be subclassed by object type
|
||||
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
|
||||
'JobTaskPermission']
|
||||
|
||||
class CustomRbac(permissions.BasePermission):
|
||||
class ModelAccessPermission(permissions.BasePermission):
|
||||
'''
|
||||
Default permissions class to check user access based on the model and
|
||||
request method, optionally verifying the request data.
|
||||
'''
|
||||
|
||||
def _check_options_permissions(self, request, view, obj=None):
|
||||
return self._check_get_permissions(request, view, obj)
|
||||
def check_options_permissions(self, request, view, obj=None):
|
||||
return self.check_get_permissions(request, view, obj)
|
||||
|
||||
def _check_head_permissions(self, request, view, obj=None):
|
||||
return self._check_get_permissions(request, view, obj)
|
||||
def check_head_permissions(self, request, view, obj=None):
|
||||
return self.check_get_permissions(request, view, obj)
|
||||
|
||||
def _check_get_permissions(self, request, view, obj=None):
|
||||
def check_get_permissions(self, request, view, obj=None):
|
||||
if hasattr(view, 'parent_model'):
|
||||
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
|
||||
if not check_user_access(request.user, view.parent_model, 'read',
|
||||
@ -30,7 +42,7 @@ class CustomRbac(permissions.BasePermission):
|
||||
return True
|
||||
return check_user_access(request.user, view.model, 'read', obj)
|
||||
|
||||
def _check_post_permissions(self, request, view, obj=None):
|
||||
def check_post_permissions(self, request, view, obj=None):
|
||||
if hasattr(view, 'parent_model'):
|
||||
parent_obj = view.parent_model.objects.get(pk=view.kwargs['pk'])
|
||||
return True
|
||||
@ -39,7 +51,7 @@ class CustomRbac(permissions.BasePermission):
|
||||
return True
|
||||
return check_user_access(request.user, view.model, 'add', request.DATA)
|
||||
|
||||
def _check_put_permissions(self, request, view, obj=None):
|
||||
def check_put_permissions(self, request, view, obj=None):
|
||||
if not obj:
|
||||
return True # FIXME: For some reason this needs to return True
|
||||
# because it is first called with obj=None?
|
||||
@ -50,13 +62,20 @@ class CustomRbac(permissions.BasePermission):
|
||||
return check_user_access(request.user, view.model, 'change', obj,
|
||||
request.DATA)
|
||||
|
||||
def _check_delete_permissions(self, request, view, obj=None):
|
||||
def check_patch_permissions(self, request, view, obj=None):
|
||||
return self.check_put_permissions(request, view, obj)
|
||||
|
||||
def check_delete_permissions(self, request, view, obj=None):
|
||||
if not obj:
|
||||
return True # FIXME: For some reason this needs to return True
|
||||
# because it is first called with obj=None?
|
||||
return check_user_access(request.user, view.model, 'delete', obj)
|
||||
|
||||
def _check_permissions(self, request, view, obj=None):
|
||||
def check_permissions(self, request, view, obj=None):
|
||||
'''
|
||||
Perform basic permissions checking before delegating to the appropriate
|
||||
method based on the request method.
|
||||
'''
|
||||
|
||||
# Check that obj (if given) is active, otherwise raise a 404.
|
||||
active = getattr(obj, 'active', getattr(obj, 'is_active', True))
|
||||
@ -79,39 +98,20 @@ class CustomRbac(permissions.BasePermission):
|
||||
|
||||
# Check permissions for the given view and object, based on the request
|
||||
# method used.
|
||||
check_method = getattr(self, '_check_%s_permissions' % \
|
||||
check_method = getattr(self, 'check_%s_permissions' % \
|
||||
request.method.lower(), None)
|
||||
result = check_method and check_method(request, view, obj)
|
||||
if not result:
|
||||
raise PermissionDenied()
|
||||
|
||||
return result
|
||||
|
||||
# If no obj is given, check list permissions.
|
||||
|
||||
if obj is None:
|
||||
if getattr(view, 'list_permissions_check', None):
|
||||
if not view.list_permissions_check(request):
|
||||
raise PermissionDenied()
|
||||
elif not getattr(view, 'item_permissions_check', None):
|
||||
raise Exception('internal error, list_permissions_check or '
|
||||
'item_permissions_check must be defined')
|
||||
return True
|
||||
|
||||
# Otherwise, check the item permissions for the given obj.
|
||||
|
||||
else:
|
||||
if not view.item_permissions_check(request, obj):
|
||||
raise PermissionDenied()
|
||||
return True
|
||||
|
||||
def has_permission(self, request, view, obj=None):
|
||||
|
||||
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
|
||||
request.user, request.method, request.DATA,
|
||||
view.__class__.__name__, obj)
|
||||
try:
|
||||
response = self._check_permissions(request, view, obj)
|
||||
response = self.check_permissions(request, view, obj)
|
||||
except Exception, e:
|
||||
logger.debug('has_permission raised %r', e, exc_info=True)
|
||||
raise
|
||||
@ -122,7 +122,7 @@ class CustomRbac(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return self.has_permission(request, view, obj)
|
||||
|
||||
class JobTemplateCallbackPermission(CustomRbac):
|
||||
class JobTemplateCallbackPermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission check used by job template callback view for requests from
|
||||
empheral hosts.
|
||||
@ -149,7 +149,7 @@ class JobTemplateCallbackPermission(CustomRbac):
|
||||
else:
|
||||
return True
|
||||
|
||||
class JobTaskPermission(CustomRbac):
|
||||
class JobTaskPermission(ModelAccessPermission):
|
||||
'''
|
||||
Permission checks used for API callbacks from running a task.
|
||||
'''
|
||||
@ -1,19 +1,26 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import cStringIO
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from celery import Task
|
||||
from django.conf import settings
|
||||
|
||||
# Pexpect
|
||||
import pexpect
|
||||
from awx.main.models import *
|
||||
|
||||
# Celery
|
||||
from celery import Task
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
|
||||
# AWX
|
||||
from awx.main.models import Job
|
||||
|
||||
__all__ = ['RunJob']
|
||||
|
||||
@ -39,7 +46,6 @@ class RunJob(Task):
|
||||
if field == 'status':
|
||||
update_fields.append('failed')
|
||||
job.save(update_fields=update_fields)
|
||||
# FIXME: Commit transaction?
|
||||
return job
|
||||
|
||||
def get_path_to(self, *args):
|
||||
@ -165,6 +171,7 @@ class RunJob(Task):
|
||||
r'Bad passphrase, try again for .*:',
|
||||
r'sudo password.*:',
|
||||
r'SSH password:',
|
||||
r'Password:',
|
||||
pexpect.TIMEOUT,
|
||||
pexpect.EOF,
|
||||
]
|
||||
@ -175,7 +182,7 @@ class RunJob(Task):
|
||||
child.sendline('')
|
||||
elif result_id == 2:
|
||||
child.sendline(passwords.get('sudo_password', ''))
|
||||
elif result_id == 3:
|
||||
elif result_id in (3, 4):
|
||||
child.sendline(passwords.get('ssh_password', ''))
|
||||
job_updates = {}
|
||||
if logfile_pos != logfile.tell():
|
||||
|
||||
@ -187,9 +187,7 @@ class CleanupDeletedTest(BaseCommandTest):
|
||||
self.assertEqual(counts_before, counts_after)
|
||||
# "Delete some users".
|
||||
for user in User.objects.all():
|
||||
user.username = "_deleted_%s_%s" % (now().isoformat(), user.username)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
user.mark_inactive()
|
||||
# With days=1, no users will be deleted.
|
||||
counts_before = self.get_user_counts()
|
||||
self.assertTrue(counts_before[1])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -147,8 +147,11 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'awx.main.permissions.ModelAccessPermission',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'awx.main.custom_filters.CustomFilterBackend',
|
||||
'awx.main.filters.DefaultFilterBackend',
|
||||
),
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
@ -323,7 +326,7 @@ LOGGING = {
|
||||
'handlers': ['console', 'file', 'syslog'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
'awx.main.rbac': {
|
||||
'awx.main.permissions': {
|
||||
'handlers': ['null'],
|
||||
# Comment the line below to show lots of permissions logging.
|
||||
'propagate': False,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user