Initial RBAC API implementation

This commit is contained in:
Akita Noek
2016-02-22 16:21:56 -05:00
parent dce474ec5e
commit b08809f7cc
9 changed files with 756 additions and 81 deletions

View File

@@ -39,6 +39,7 @@ from polymorphic import PolymorphicModel
# AWX # AWX
from awx.main.constants import SCHEDULEABLE_PROVIDERS from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.fields import ImplicitRoleField
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat
from awx.main.redact import REPLACE_STR from awx.main.redact import REPLACE_STR
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
@@ -201,7 +202,7 @@ class BaseSerializer(serializers.ModelSerializer):
__metaclass__ = BaseSerializerMetaclass __metaclass__ = BaseSerializerMetaclass
class Meta: class Meta:
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created', fields = ('id', 'type', 'resource_id', 'url', 'related', 'summary_fields', 'created',
'modified', 'name', 'description') 'modified', 'name', 'description')
summary_fields = () # FIXME: List of field names from this serializer that should be used when included as part of another's summary_fields. summary_fields = () # FIXME: List of field names from this serializer that should be used when included as part of another's summary_fields.
summarizable_fields = () # FIXME: List of field names on this serializer that should be included in summary_fields. summarizable_fields = () # FIXME: List of field names on this serializer that should be included in summary_fields.
@@ -216,6 +217,8 @@ class BaseSerializer(serializers.ModelSerializer):
created = serializers.SerializerMethodField() created = serializers.SerializerMethodField()
modified = serializers.SerializerMethodField() modified = serializers.SerializerMethodField()
active = serializers.SerializerMethodField() active = serializers.SerializerMethodField()
resource_id = serializers.SerializerMethodField()
def get_type(self, obj): def get_type(self, obj):
return get_type_for_model(self.Meta.model) return get_type_for_model(self.Meta.model)
@@ -254,6 +257,8 @@ class BaseSerializer(serializers.ModelSerializer):
res['created_by'] = reverse('api:user_detail', args=(obj.created_by.pk,)) res['created_by'] = reverse('api:user_detail', args=(obj.created_by.pk,))
if getattr(obj, 'modified_by', None) and obj.modified_by.is_active: if getattr(obj, 'modified_by', None) and obj.modified_by.is_active:
res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,)) res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,))
if isinstance(obj, ResourceMixin):
res['resource'] = reverse('api:resource_detail', args=(obj.resource_id,))
return res return res
def _get_summary_fields(self, obj): def _get_summary_fields(self, obj):
@@ -304,8 +309,30 @@ class BaseSerializer(serializers.ModelSerializer):
summary_fields['modified_by'] = OrderedDict() summary_fields['modified_by'] = OrderedDict()
for field in SUMMARIZABLE_FK_FIELDS['user']: for field in SUMMARIZABLE_FK_FIELDS['user']:
summary_fields['modified_by'][field] = getattr(obj.modified_by, field) summary_fields['modified_by'][field] = getattr(obj.modified_by, field)
# RBAC summary fields
request = self.context.get('request', None)
if request and isinstance(obj, ResourceMixin) and request.user.is_authenticated():
summary_fields['permissions'] = obj.get_permissions(request.user)
roles = {}
for field in obj._meta.get_fields():
if type(field) is ImplicitRoleField:
role = getattr(obj, field.name)
#roles[field.name] = RoleSerializer(data=role).to_representation(role)
roles[field.name] = {
'id': role.id,
'name': role.name,
'url': role.get_absolute_url(),
}
if len(roles) > 0:
summary_fields['roles'] = roles
return summary_fields return summary_fields
def get_resource_id(self, obj):
if isinstance(obj, ResourceMixin):
return obj.resource.id
return None
def get_created(self, obj): def get_created(self, obj):
if obj is None: if obj is None:
return None return None
@@ -479,6 +506,8 @@ class BaseSerializer(serializers.ModelSerializer):
# set by the sub list create view. # set by the sub list create view.
if parent_key and hasattr(view, '_raw_data_form_marker'): if parent_key and hasattr(view, '_raw_data_form_marker'):
ret.pop(parent_key, None) ret.pop(parent_key, None)
if 'resource_id' in ret and ret['resource_id'] is None:
ret.pop('resource_id')
return ret return ret
@@ -737,7 +766,7 @@ class UserSerializer(BaseSerializer):
admin_of_organizations = reverse('api:user_admin_of_organizations_list', args=(obj.pk,)), admin_of_organizations = reverse('api:user_admin_of_organizations_list', args=(obj.pk,)),
projects = reverse('api:user_projects_list', args=(obj.pk,)), projects = reverse('api:user_projects_list', args=(obj.pk,)),
credentials = reverse('api:user_credentials_list', args=(obj.pk,)), credentials = reverse('api:user_credentials_list', args=(obj.pk,)),
permissions = reverse('api:user_permissions_list', args=(obj.pk,)), roles = reverse('api:user_roles_list', args=(obj.pk,)),
activity_stream = reverse('api:user_activity_stream_list', args=(obj.pk,)), activity_stream = reverse('api:user_activity_stream_list', args=(obj.pk,)),
)) ))
return res return res
@@ -1369,7 +1398,7 @@ class TeamSerializer(BaseSerializer):
projects = reverse('api:team_projects_list', args=(obj.pk,)), projects = reverse('api:team_projects_list', args=(obj.pk,)),
users = reverse('api:team_users_list', args=(obj.pk,)), users = reverse('api:team_users_list', args=(obj.pk,)),
credentials = reverse('api:team_credentials_list', args=(obj.pk,)), credentials = reverse('api:team_credentials_list', args=(obj.pk,)),
permissions = reverse('api:team_permissions_list', args=(obj.pk,)), roles = reverse('api:team_roles_list', args=(obj.pk,)),
activity_stream = reverse('api:team_activity_stream_list', args=(obj.pk,)), activity_stream = reverse('api:team_activity_stream_list', args=(obj.pk,)),
)) ))
if obj.organization and obj.organization.active: if obj.organization and obj.organization.active:
@@ -1383,56 +1412,70 @@ class TeamSerializer(BaseSerializer):
return ret return ret
class PermissionSerializer(BaseSerializer):
class RoleSerializer(BaseSerializer):
class Meta: class Meta:
model = Permission model = Role
fields = ('*', 'user', 'team', 'project', 'inventory', fields = ('*',)
'permission_type', 'run_ad_hoc_commands')
def get_related(self, obj): def get_related(self, obj):
res = super(PermissionSerializer, self).get_related(obj) ret = super(RoleSerializer, self).get_related(obj)
if obj.user and obj.user.is_active: if obj.content_object:
res['user'] = reverse('api:user_detail', args=(obj.user.pk,)) if type(obj.content_object) is Organization:
if obj.team and obj.team.active: ret['organization'] = reverse('api:organization_detail', args=(obj.object_id,))
res['team'] = reverse('api:team_detail', args=(obj.team.pk,)) if type(obj.content_object) is Team:
if obj.project and obj.project.active: ret['team'] = reverse('api:team_detail', args=(obj.object_id,))
res['project'] = reverse('api:project_detail', args=(obj.project.pk,)) if type(obj.content_object) is Project:
if obj.inventory and obj.inventory.active: ret['project'] = reverse('api:project_detail', args=(obj.object_id,))
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) if type(obj.content_object) is Inventory:
return res ret['inventory'] = reverse('api:inventory_detail', args=(obj.object_id,))
if type(obj.content_object) is Host:
ret['host'] = reverse('api:host_detail', args=(obj.object_id,))
if type(obj.content_object) is Group:
ret['group'] = reverse('api:group_detail', args=(obj.object_id,))
if type(obj.content_object) is InventorySource:
ret['inventory_source'] = reverse('api:inventory_source_detail', args=(obj.object_id,))
if type(obj.content_object) is Credential:
ret['credential'] = reverse('api:credential_detail', args=(obj.object_id,))
if type(obj.content_object) is JobTemplate:
ret['job_template'] = reverse('api:job_template_detail', args=(obj.object_id,))
def validate(self, attrs):
# Can only set either user or team.
if attrs.get('user', None) and attrs.get('team', None):
raise serializers.ValidationError('permission can only be assigned'
' to a user OR a team, not both')
# Cannot assign admit/read/write permissions for a project.
if attrs.get('permission_type', None) in ('admin', 'read', 'write') and attrs.get('project', None):
raise serializers.ValidationError('project cannot be assigned for '
'inventory-only permissions')
# Project is required when setting deployment permissions.
if attrs.get('permission_type', None) in ('run', 'check') and not attrs.get('project', None):
raise serializers.ValidationError('project is required when '
'assigning deployment permissions')
return super(PermissionSerializer, self).validate(attrs)
def to_representation(self, obj):
ret = super(PermissionSerializer, self).to_representation(obj)
if obj is None:
return ret
if 'user' in ret and (not obj.user or not obj.user.is_active):
ret['user'] = None
if 'team' in ret and (not obj.team or not obj.team.active):
ret['team'] = None
if 'project' in ret and (not obj.project or not obj.project.active):
ret['project'] = None
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
ret['inventory'] = None
return ret return ret
class ResourceSerializer(BaseSerializer):
class Meta:
model = Resource
fields = ('*',)
class ResourceAccessListElementSerializer(UserSerializer):
def to_representation(self, user):
ret = super(ResourceAccessListElementSerializer, self).to_representation(user)
resource_id = self.context['view'].resource_id
resource = Resource.objects.get(pk=resource_id)
if 'summary_fields' not in ret:
ret['summary_fields'] = {}
ret['summary_fields']['permissions'] = resource.get_permissions(user)
def format_role_perm(role):
return { 'role': { 'id': role.id, 'name': role.name}, 'permissions': resource.get_role_permissions(role)}
direct_permissive_role_ids = resource.permissions.values_list('role__id')
direct_access_roles = user.roles.filter(id__in=direct_permissive_role_ids).all()
ret['summary_fields']['direct_access'] = [format_role_perm(r) for r in direct_access_roles]
all_permissive_role_ids = resource.permissions.values_list('role__ancestors__id')
indirect_access_roles = user.roles.filter(id__in=all_permissive_role_ids).exclude(id__in=direct_permissive_role_ids).all()
ret['summary_fields']['indirect_access'] = [format_role_perm(r) for r in indirect_access_roles]
return ret
class CredentialSerializer(BaseSerializer): class CredentialSerializer(BaseSerializer):
# FIXME: may want to make some fields filtered based on user accessing # FIXME: may want to make some fields filtered based on user accessing

View File

@@ -30,7 +30,7 @@ user_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/admin_of_organizations/$', 'user_admin_of_organizations_list'), url(r'^(?P<pk>[0-9]+)/admin_of_organizations/$', 'user_admin_of_organizations_list'),
url(r'^(?P<pk>[0-9]+)/projects/$', 'user_projects_list'), url(r'^(?P<pk>[0-9]+)/projects/$', 'user_projects_list'),
url(r'^(?P<pk>[0-9]+)/credentials/$', 'user_credentials_list'), url(r'^(?P<pk>[0-9]+)/credentials/$', 'user_credentials_list'),
url(r'^(?P<pk>[0-9]+)/permissions/$', 'user_permissions_list'), url(r'^(?P<pk>[0-9]+)/roles/$', 'user_roles_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'user_activity_stream_list'), url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'user_activity_stream_list'),
) )
@@ -58,7 +58,7 @@ team_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/projects/$', 'team_projects_list'), url(r'^(?P<pk>[0-9]+)/projects/$', 'team_projects_list'),
url(r'^(?P<pk>[0-9]+)/users/$', 'team_users_list'), url(r'^(?P<pk>[0-9]+)/users/$', 'team_users_list'),
url(r'^(?P<pk>[0-9]+)/credentials/$', 'team_credentials_list'), url(r'^(?P<pk>[0-9]+)/credentials/$', 'team_credentials_list'),
url(r'^(?P<pk>[0-9]+)/permissions/$', 'team_permissions_list'), url(r'^(?P<pk>[0-9]+)/roles/$', 'team_roles_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'team_activity_stream_list'), url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'team_activity_stream_list'),
) )
@@ -141,8 +141,22 @@ credential_urls = patterns('awx.api.views',
# See also credentials resources on users/teams. # See also credentials resources on users/teams.
) )
permission_urls = patterns('awx.api.views', role_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'permission_detail'), url(r'^$', 'role_list'),
url(r'^(?P<pk>[0-9]+)/$', 'role_detail'),
url(r'^(?P<pk>[0-9]+)/users/$', 'role_users_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'role_teams_list'),
url(r'^(?P<pk>[0-9]+)/parents/$', 'role_parents_list'),
url(r'^(?P<pk>[0-9]+)/children/$', 'role_children_list'),
)
resource_urls = patterns('awx.api.views',
#url(r'^$', 'resource_list'),
url(r'^(?P<pk>[0-9]+)/$', 'resource_detail'),
url(r'^(?P<pk>[0-9]+)/access_list/$', 'resource_access_list'),
#url(r'^(?P<pk>[0-9]+)/users/$', 'resource_users_list'),
#url(r'^(?P<pk>[0-9]+)/teams/$', 'resource_teams_list'),
#url(r'^(?P<pk>[0-9]+)/roles/$', 'resource_teams_list'),
) )
job_template_urls = patterns('awx.api.views', job_template_urls = patterns('awx.api.views',
@@ -249,7 +263,8 @@ v1_urls = patterns('awx.api.views',
url(r'^inventory_updates/', include(inventory_update_urls)), url(r'^inventory_updates/', include(inventory_update_urls)),
url(r'^inventory_scripts/', include(inventory_script_urls)), url(r'^inventory_scripts/', include(inventory_script_urls)),
url(r'^credentials/', include(credential_urls)), url(r'^credentials/', include(credential_urls)),
url(r'^permissions/', include(permission_urls)), url(r'^roles/', include(role_urls)),
url(r'^resources/', include(resource_urls)),
url(r'^job_templates/', include(job_template_urls)), url(r'^job_templates/', include(job_template_urls)),
url(r'^jobs/', include(job_urls)), url(r'^jobs/', include(job_urls)),
url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)),

View File

@@ -700,24 +700,29 @@ class TeamUsersList(SubListCreateAttachDetachAPIView):
parent_model = Team parent_model = Team
relationship = 'users' relationship = 'users'
class TeamPermissionsList(SubListCreateAttachDetachAPIView):
model = Permission class TeamRolesList(SubListCreateAttachDetachAPIView):
serializer_class = PermissionSerializer
model = Role
serializer_class = RoleSerializer
parent_model = Team parent_model = Team
relationship = 'permissions' relationship='member_role.children'
parent_key = 'team'
def get_queryset(self): def get_queryset(self):
# FIXME: Default get_queryset should handle this. # XXX: This needs to be the intersection between
# what roles the user has and what roles the viewer
# has access to see.
team = Team.objects.get(pk=self.kwargs['pk']) team = Team.objects.get(pk=self.kwargs['pk'])
base = Permission.objects.filter(team = team) return team.member_role.children
#if Team.can_user_administrate(self.request.user, team, None):
if self.request.user.can_access(Team, 'change', team, None): # XXX: Need to enforce permissions
return base def post(self, request, *args, **kwargs):
elif team.users.filter(pk=self.request.user.pk).count() > 0: # Forbid implicit role creation here
return base sub_id = request.data.get('id', None)
raise PermissionDenied() if not sub_id:
data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs)
class TeamProjectsList(SubListCreateAttachDetachAPIView): class TeamProjectsList(SubListCreateAttachDetachAPIView):
@@ -920,13 +925,30 @@ class UserTeamsList(SubListAPIView):
parent_model = User parent_model = User
relationship = 'teams' relationship = 'teams'
class UserPermissionsList(SubListCreateAttachDetachAPIView):
model = Permission class UserRolesList(SubListCreateAttachDetachAPIView):
serializer_class = PermissionSerializer
model = Role
serializer_class = RoleSerializer
parent_model = User parent_model = User
relationship = 'permissions' relationship='roles'
parent_key = 'user'
def get_queryset(self):
# XXX: This needs to be the intersection between
# what roles the user has and what roles the viewer
# has access to see.
u = User.objects.get(pk=self.kwargs['pk'])
return u.roles
def post(self, request, *args, **kwargs):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs)
class UserProjectsList(SubListAPIView): class UserProjectsList(SubListAPIView):
@@ -1047,10 +1069,6 @@ class CredentialActivityStreamList(SubListAPIView):
# Okay, let it through. # Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs) return super(type(self), self).get(request, *args, **kwargs)
class PermissionDetail(RetrieveUpdateDestroyAPIView):
model = Permission
serializer_class = PermissionSerializer
class InventoryScriptList(ListCreateAPIView): class InventoryScriptList(ListCreateAPIView):
@@ -3031,6 +3049,134 @@ class SettingsReset(APIView):
TowerSettings.objects.filter(key=settings_key).delete() TowerSettings.objects.filter(key=settings_key).delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
#class RoleList(ListCreateAPIView):
class RoleList(ListAPIView):
model = Role
serializer_class = RoleSerializer
new_in_300 = True
# XXX: Permissions - only roles the user has access to see should be listed here
def get_queryset(self):
return Role.objects
# XXX: Need to define who can create custom roles, and then restrict access
# appropriately
# XXX: Need to define how we want to deal with administration of custom roles.
class RoleDetail(RetrieveUpdateAPIView):
model = Role
serializer_class = RoleSerializer
new_in_300 = True
# XXX: Permissions - only appropriate people should be able to change these
class RoleUsersList(SubListCreateAttachDetachAPIView):
model = User
serializer_class = UserSerializer
parent_model = Role
relationship = 'members'
def get_queryset(self):
# XXX: Access control
role = Role.objects.get(pk=self.kwargs['pk'])
return role.members
def post(self, request, *args, **kwargs):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(type(self), self).post(request, *args, **kwargs)
class RoleTeamsList(ListAPIView):
model = Team
serializer_class = TeamSerializer
parent_model = Role
relationship = 'member_role.parents'
def get_queryset(self):
# TODO: Check
role = Role.objects.get(pk=self.kwargs['pk'])
return Team.objects.filter(member_role__children__in=[role])
def post(self, request, pk, *args, **kwargs):
# Forbid implicit role creation here
sub_id = request.data.get('id', None)
if not sub_id:
data = dict(msg='Role "id" field is missing')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
# XXX: Need to pull in can_attach and can_unattach kinda code from SubListCreateAttachDetachAPIView
role = Role.objects.get(pk=self.kwargs['pk'])
team = Team.objects.get(pk=sub_id)
if request.data.get('disassociate', None):
team.member_role.children.remove(role)
else:
team.member_role.children.add(role)
return Response(status=status.HTTP_204_NO_CONTENT)
# XXX attach/detach needs to ensure we have the appropriate perms
class RoleParentsList(SubListAPIView):
model = Role
serializer_class = RoleSerializer
parent_model = Role
relationship = 'parents'
def get_queryset(self):
# XXX: This should be the intersection between the roles of the user
# and the roles that the requesting user has access to see
role = Role.objects.get(pk=self.kwargs['pk'])
return role.parents
class RoleChildrenList(SubListAPIView):
model = Role
serializer_class = RoleSerializer
parent_model = Role
relationship = 'children'
def get_queryset(self):
# XXX: This should be the intersection between the roles of the user
# and the roles that the requesting user has access to see
role = Role.objects.get(pk=self.kwargs['pk'])
return role.children
class ResourceDetail(RetrieveAPIView):
model = Resource
serializer_class = ResourceSerializer
new_in_300 = True
# XXX: Permissions - only roles the user has access to see should be listed here
def get_queryset(self):
return Resource.objects
class ResourceAccessList(ListAPIView):
model = User
serializer_class = ResourceAccessListElementSerializer
new_in_300 = True
def get_queryset(self):
self.resource_id = self.kwargs['pk']
resource = Resource.objects.get(pk=self.kwargs['pk'])
roles = set([p.role for p in resource.permissions.all()])
ancestors = set()
for r in roles:
ancestors.update(set(r.ancestors.all()))
return User.objects.filter(roles__in=list(ancestors))
# Create view functions for all of the class-based views to simplify inclusion # Create view functions for all of the class-based views to simplify inclusion
# in URL patterns and reverse URL lookups, converting CamelCase names to # in URL patterns and reverse URL lookups, converting CamelCase names to
# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). # lowercase_with_underscore (e.g. MyView.as_view() becomes my_view).

View File

@@ -1659,6 +1659,64 @@ class TowerSettingsAccess(BaseAccess):
def can_delete(self, obj): def can_delete(self, obj):
return self.user.is_superuser return self.user.is_superuser
class RoleAccess(BaseAccess):
'''
TODO: XXX: Needs implemenation
'''
model = Role
def get_queryset(self):
if self.user.is_superuser:
return self.model.objects.all()
return self.model.objects.none()
def can_change(self, obj, data):
return self.user.is_superuser
def can_add(self, obj, data):
return self.user.is_superuser
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
return self.user.is_superuser
def can_unattach(self, obj, sub_obj, relationship):
return self.user.is_superuser
def can_delete(self, obj):
return self.user.is_superuser
class ResourceAccess(BaseAccess):
'''
TODO: XXX: Needs implemenation
'''
model = Role
def get_queryset(self):
if self.user.is_superuser:
return self.model.objects.all()
return self.model.objects.none()
def can_change(self, obj, data):
return self.user.is_superuser
def can_add(self, obj, data):
return self.user.is_superuser
def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False):
return self.user.is_superuser
def can_unattach(self, obj, sub_obj, relationship):
return self.user.is_superuser
def can_delete(self, obj):
return self.user.is_superuser
register_access(User, UserAccess) register_access(User, UserAccess)
register_access(Organization, OrganizationAccess) register_access(Organization, OrganizationAccess)
register_access(Inventory, InventoryAccess) register_access(Inventory, InventoryAccess)
@@ -1685,3 +1743,5 @@ register_access(UnifiedJob, UnifiedJobAccess)
register_access(ActivityStream, ActivityStreamAccess) register_access(ActivityStream, ActivityStreamAccess)
register_access(CustomInventoryScript, CustomInventoryScriptAccess) register_access(CustomInventoryScript, CustomInventoryScriptAccess)
register_access(TowerSettings, TowerSettingsAccess) register_access(TowerSettings, TowerSettingsAccess)
register_access(Role, RoleAccess)
register_access(Resource, ResourceAccess)

View File

@@ -18,6 +18,7 @@ from awx.main.models.activity_stream import * # noqa
from awx.main.models.ha import * # noqa from awx.main.models.ha import * # noqa
from awx.main.models.configuration import * # noqa from awx.main.models.configuration import * # noqa
from awx.main.models.rbac import * # noqa from awx.main.models.rbac import * # noqa
from awx.main.models.mixins import * # noqa
# Monkeypatch Django serializer to ignore django-taggit fields (which break # Monkeypatch Django serializer to ignore django-taggit fields (which break
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155). # the dumpdata command; see https://github.com/alex/django-taggit/issues/155).

View File

@@ -156,6 +156,35 @@ class Resource(CommonModelNameNotUnique):
return {k[4:]:v for k,v in res[0].items()} return {k[4:]:v for k,v in res[0].items()}
return None return None
def get_role_permissions(self, role):
'''
Returns a dict (or None) of the permissions a role has for a given
resource.
Note: Each field in the dict is the `or` of all respective permissions
that have been granted to either the role or any descendents of that role.
'''
qs = Role.objects.filter(id=role.id, descendents__permissions__resource=self)
qs = qs.annotate(max_create = Max('descendents__permissions__create'))
qs = qs.annotate(max_read = Max('descendents__permissions__read'))
qs = qs.annotate(max_write = Max('descendents__permissions__write'))
qs = qs.annotate(max_update = Max('descendents__permissions__update'))
qs = qs.annotate(max_delete = Max('descendents__permissions__delete'))
qs = qs.annotate(max_scm_update = Max('descendents__permissions__scm_update'))
qs = qs.annotate(max_execute = Max('descendents__permissions__execute'))
qs = qs.annotate(max_use = Max('descendents__permissions__use'))
qs = qs.values('max_create', 'max_read', 'max_write', 'max_update',
'max_delete', 'max_scm_update', 'max_execute', 'max_use')
res = qs.all()
if len(res):
# strip away the 'max_' prefix
return {k[4:]:v for k,v in res[0].items()}
return None
class RolePermission(CreatedModifiedModel): class RolePermission(CreatedModifiedModel):
''' '''

View File

@@ -338,7 +338,6 @@ model_serializer_mapping = {
Credential: CredentialSerializer, Credential: CredentialSerializer,
Team: TeamSerializer, Team: TeamSerializer,
Project: ProjectSerializer, Project: ProjectSerializer,
Permission: PermissionSerializer,
JobTemplate: JobTemplateSerializer, JobTemplate: JobTemplateSerializer,
Job: JobSerializer, Job: JobSerializer,
AdHocCommand: AdHocCommandSerializer, AdHocCommand: AdHocCommandSerializer,

View File

@@ -2,9 +2,6 @@ import pytest
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from django.utils.six.moves.urllib.parse import urlparse from django.utils.six.moves.urllib.parse import urlparse
from awx.main.models.organization import Organization
from awx.main.models.ha import Instance
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework.test import ( from rest_framework.test import (
@@ -25,6 +22,9 @@ from awx.main.models.organization import (
Team, Team,
) )
from awx.main.models.rbac import Role
@pytest.fixture @pytest.fixture
def user(): def user():
def u(name, is_superuser=False): def u(name, is_superuser=False):
@@ -89,6 +89,22 @@ def credential():
def inventory(organization): def inventory(organization):
return Inventory.objects.create(name="test-inventory", organization=organization) return Inventory.objects.create(name="test-inventory", organization=organization)
@pytest.fixture
def role():
return Role.objects.create(name='role')
@pytest.fixture
def admin(user):
return user('admin', True)
@pytest.fixture
def alice(user):
return user('alice', False)
@pytest.fixture
def bob(user):
return user('bob', False)
@pytest.fixture @pytest.fixture
def group(inventory): def group(inventory):
def g(name): def g(name):
@@ -108,6 +124,7 @@ def permissions():
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,}, 'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
} }
@pytest.fixture @pytest.fixture
def post(): def post():
def rf(url, data, user=None, middleware=None, **kwargs): def rf(url, data, user=None, middleware=None, **kwargs):

View File

@@ -0,0 +1,365 @@
import mock # noqa
import pytest
from django.core.urlresolvers import reverse
from awx.main.models.rbac import Role
def mock_feature_enabled(feature, bypass_database=None):
return True
#@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
#
# /roles
#
@pytest.mark.django_db
def test_get_roles_list_admin(organization, get, admin):
'Admin can see list of all roles'
url = reverse('api:role_list')
response = get(url, admin)
assert response.status_code == 200
roles = response.data
assert roles['count'] > 0
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Unimplemented')
def test_get_roles_list_user(organization, get, user):
'Users can see all roles they have access to, but not all roles'
assert False
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_create_role(post, admin):
'Admins can create new roles'
#u = user('admin', True)
response = post(reverse('api:role_list'), {'name': 'New Role'}, admin)
assert response.status_code == 201
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_delete_role(post, admin):
'Admins can delete a custom role'
assert False
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_user_create_role(organization, get, user):
'User can create custom roles'
assert False
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_user_delete_role(organization, get, user):
'User can delete their custom roles, but not any old row'
assert False
#
# /user/<id>/roles
#
@pytest.mark.django_db
def test_get_user_roles_list(get, admin):
url = reverse('api:user_roles_list', args=(admin.id,))
response = get(url, admin)
assert response.status_code == 200
roles = response.data
assert roles['count'] > 0 # 'System Administrator' role if nothing else
@pytest.mark.django_db
def test_add_role_to_user(role, post, admin):
assert admin.roles.filter(id=role.id).count() == 0
url = reverse('api:user_roles_list', args=(admin.id,))
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert admin.roles.filter(id=role.id).count() == 1
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert admin.roles.filter(id=role.id).count() == 1
response = post(url, {}, admin)
assert response.status_code == 400
assert admin.roles.filter(id=role.id).count() == 1
@pytest.mark.django_db
def test_remove_role_from_user(role, post, admin):
assert admin.roles.filter(id=role.id).count() == 0
url = reverse('api:user_roles_list', args=(admin.id,))
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert admin.roles.filter(id=role.id).count() == 1
response = post(url, {'disassociate': role.id, 'id': role.id}, admin)
assert response.status_code == 204
assert admin.roles.filter(id=role.id).count() == 0
#
# /team/<id>/roles
#
@pytest.mark.django_db
def test_get_teams_roles_list(get, team, organization, admin):
team.member_role.children.add(organization.admin_role)
url = reverse('api:team_roles_list', args=(team.id,))
response = get(url, admin)
assert response.status_code == 200
roles = response.data
assert roles['count'] == 1
assert roles['results'][0]['id'] == organization.admin_role.id
@pytest.mark.django_db
def test_add_role_to_teams(team, role, post, admin):
assert team.member_role.children.filter(id=role.id).count() == 0
url = reverse('api:team_roles_list', args=(team.id,))
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert team.member_role.children.filter(id=role.id).count() == 1
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert team.member_role.children.filter(id=role.id).count() == 1
response = post(url, {}, admin)
assert response.status_code == 400
assert team.member_role.children.filter(id=role.id).count() == 1
@pytest.mark.django_db
def test_remove_role_from_teams(team, role, post, admin):
assert team.member_role.children.filter(id=role.id).count() == 0
url = reverse('api:team_roles_list', args=(team.id,))
response = post(url, {'id': role.id}, admin)
assert response.status_code == 204
assert team.member_role.children.filter(id=role.id).count() == 1
response = post(url, {'disassociate': role.id, 'id': role.id}, admin)
assert response.status_code == 204
assert team.member_role.children.filter(id=role.id).count() == 0
#
# /roles/<id>/
#
@pytest.mark.django_db
def test_get_role(get, admin, role):
url = reverse('api:role_detail', args=(role.id,))
response = get(url, admin)
assert response.status_code == 200
assert response.data['id'] == role.id
@pytest.mark.django_db
def test_put_role(put, admin, role):
url = reverse('api:role_detail', args=(role.id,))
response = put(url, {'name': 'Some new name'}, admin)
assert response.status_code == 200
r = Role.objects.get(id=role.id)
assert r.name == 'Some new name'
@pytest.mark.django_db
def test_put_role_access_denied(put, alice, admin, role):
url = reverse('api:role_detail', args=(role.id,))
response = put(url, {'name': 'Some new name'}, alice)
assert response.status_code == 403
#
# /roles/<id>/users/
#
@pytest.mark.django_db
def test_get_role_users(get, admin, role):
role.members.add(admin)
url = reverse('api:role_users_list', args=(role.id,))
response = get(url, admin)
assert response.status_code == 200
assert response.data['count'] == 1
assert response.data['results'][0]['id'] == admin.id
@pytest.mark.django_db
def test_add_user_to_role(post, admin, role):
url = reverse('api:role_users_list', args=(role.id,))
assert role.members.filter(id=admin.id).count() == 0
post(url, {'id': admin.id}, admin)
assert role.members.filter(id=admin.id).count() == 1
@pytest.mark.django_db
def test_remove_user_to_role(post, admin, role):
role.members.add(admin)
url = reverse('api:role_users_list', args=(role.id,))
assert role.members.filter(id=admin.id).count() == 1
post(url, {'disassociate': True, 'id': admin.id}, admin)
assert role.members.filter(id=admin.id).count() == 0
#
# /roles/<id>/teams/
#
@pytest.mark.django_db
def test_get_role_teams(get, team, admin, role):
role.parents.add(team.member_role)
url = reverse('api:role_teams_list', args=(role.id,))
response = get(url, admin)
print(response.data)
assert response.status_code == 200
assert response.data['count'] == 1
assert response.data['results'][0]['id'] == team.id
@pytest.mark.django_db
def test_add_team_to_role(post, team, admin, role):
url = reverse('api:role_teams_list', args=(role.id,))
assert role.members.filter(id=admin.id).count() == 0
res = post(url, {'id': team.id}, admin)
print res.data
assert res.status_code == 204
assert role.parents.filter(id=team.member_role.id).count() == 1
@pytest.mark.django_db
def test_remove_team_from_role(post, team, admin, role):
role.members.add(admin)
url = reverse('api:role_teams_list', args=(role.id,))
assert role.members.filter(id=admin.id).count() == 1
res = post(url, {'disassociate': True, 'id': team.id}, admin)
print res.data
assert res.status_code == 204
assert role.parents.filter(id=team.member_role.id).count() == 0
#
# /roles/<id>/parents/
#
@pytest.mark.django_db
def test_role_parents(get, team, admin, role):
role.parents.add(team.member_role)
url = reverse('api:role_parents_list', args=(role.id,))
response = get(url, admin)
assert response.status_code == 200
assert response.data['count'] == 1
assert response.data['results'][0]['id'] == team.member_role.id
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_role_add_parent(post, team, admin, role):
assert role.parents.count() == 0
url = reverse('api:role_parents_list', args=(role.id,))
post(url, {'id': team.member_role.id}, admin)
assert role.parents.count() == 1
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_role_remove_parent(post, team, admin, role):
role.parents.add(team.member_role)
assert role.parents.count() == 1
url = reverse('api:role_parents_list', args=(role.id,))
post(url, {'disassociate': True, 'id': team.member_role.id}, admin)
assert role.parents.count() == 0
#
# /roles/<id>/children/
#
@pytest.mark.django_db
def test_role_children(get, team, admin, role):
role.parents.add(team.member_role)
url = reverse('api:role_children_list', args=(team.member_role.id,))
response = get(url, admin)
assert response.status_code == 200
assert response.data['count'] == 1
assert response.data['results'][0]['id'] == role.id
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_role_add_children(post, team, admin, role):
assert role.children.count() == 0
url = reverse('api:role_children_list', args=(role.id,))
post(url, {'id': team.member_role.id}, admin)
assert role.children.count() == 1
@pytest.mark.django_db
@pytest.mark.skipif(True, reason='Waiting on custom role requirements')
def test_role_remove_children(post, team, admin, role):
role.children.add(team.member_role)
assert role.children.count() == 1
url = reverse('api:role_children_list', args=(role.id,))
post(url, {'disassociate': True, 'id': team.member_role.id}, admin)
assert role.children.count() == 0
#
# /resource/<id>/access_list
#
@pytest.mark.django_db
def test_resource_access_list(get, team, admin, role):
team.users.add(admin)
url = reverse('api:resource_access_list', args=(team.resource.id,))
res = get(url, admin)
assert res.status_code == 200
#
# Generics
#
@pytest.mark.django_db
def test_ensure_rbac_fields_are_present(organization, get, admin):
url = reverse('api:organization_detail', args=(organization.id,))
response = get(url, admin)
assert response.status_code == 200
org = response.data
assert 'summary_fields' in org
assert 'resource_id' in org
assert org['resource_id'] > 0
assert org['related']['resource'] != ''
assert 'roles' in org['summary_fields']
org_role_response = get(org['summary_fields']['roles']['admin_role']['url'], admin)
assert org_role_response.status_code == 200
role = org_role_response.data
assert role['related']['organization'] == url
@pytest.mark.django_db
def test_ensure_permissions_is_present(organization, get, user):
#u = user('admin', True)
url = reverse('api:organization_detail', args=(organization.id,))
response = get(url, user('admin', True))
assert response.status_code == 200
org = response.data
assert 'summary_fields' in org
assert 'permissions' in org['summary_fields']
assert org['summary_fields']['permissions']['read'] > 0
@pytest.mark.django_db
def test_ensure_role_summary_is_present(organization, get, user):
#u = user('admin', True)
url = reverse('api:organization_detail', args=(organization.id,))
response = get(url, user('admin', True))
assert response.status_code == 200
org = response.data
assert 'summary_fields' in org
assert 'roles' in org['summary_fields']
assert org['summary_fields']['roles']['admin_role']['id'] > 0