mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
Initial RBAC API implementation
This commit is contained in:
parent
dce474ec5e
commit
b08809f7cc
@ -39,6 +39,7 @@ from polymorphic import PolymorphicModel
|
||||
# AWX
|
||||
from awx.main.constants import SCHEDULEABLE_PROVIDERS
|
||||
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.redact import REPLACE_STR
|
||||
from awx.main.conf import tower_settings
|
||||
@ -127,7 +128,7 @@ class BaseSerializerMetaclass(serializers.SerializerMetaclass):
|
||||
'foo': {'required': False, 'default': ''},
|
||||
'bar': {'label': 'New Label for Bar'},
|
||||
}
|
||||
|
||||
|
||||
# The resulting value of extra_kwargs would be:
|
||||
extra_kwargs = {
|
||||
'foo': {'required': False, 'default': ''},
|
||||
@ -201,7 +202,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
__metaclass__ = BaseSerializerMetaclass
|
||||
|
||||
class Meta:
|
||||
fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created',
|
||||
fields = ('id', 'type', 'resource_id', 'url', 'related', 'summary_fields', 'created',
|
||||
'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.
|
||||
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()
|
||||
modified = serializers.SerializerMethodField()
|
||||
active = serializers.SerializerMethodField()
|
||||
resource_id = serializers.SerializerMethodField()
|
||||
|
||||
|
||||
def get_type(self, obj):
|
||||
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,))
|
||||
if getattr(obj, 'modified_by', None) and obj.modified_by.is_active:
|
||||
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
|
||||
|
||||
def _get_summary_fields(self, obj):
|
||||
@ -304,8 +309,30 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
summary_fields['modified_by'] = OrderedDict()
|
||||
for field in SUMMARIZABLE_FK_FIELDS['user']:
|
||||
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
|
||||
|
||||
def get_resource_id(self, obj):
|
||||
if isinstance(obj, ResourceMixin):
|
||||
return obj.resource.id
|
||||
return None
|
||||
|
||||
def get_created(self, obj):
|
||||
if obj is None:
|
||||
return None
|
||||
@ -479,6 +506,8 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
# set by the sub list create view.
|
||||
if parent_key and hasattr(view, '_raw_data_form_marker'):
|
||||
ret.pop(parent_key, None)
|
||||
if 'resource_id' in ret and ret['resource_id'] is None:
|
||||
ret.pop('resource_id')
|
||||
return ret
|
||||
|
||||
|
||||
@ -737,7 +766,7 @@ class UserSerializer(BaseSerializer):
|
||||
admin_of_organizations = reverse('api:user_admin_of_organizations_list', args=(obj.pk,)),
|
||||
projects = reverse('api:user_projects_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,)),
|
||||
))
|
||||
return res
|
||||
@ -1369,7 +1398,7 @@ class TeamSerializer(BaseSerializer):
|
||||
projects = reverse('api:team_projects_list', args=(obj.pk,)),
|
||||
users = reverse('api:team_users_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,)),
|
||||
))
|
||||
if obj.organization and obj.organization.active:
|
||||
@ -1383,56 +1412,70 @@ class TeamSerializer(BaseSerializer):
|
||||
return ret
|
||||
|
||||
|
||||
class PermissionSerializer(BaseSerializer):
|
||||
|
||||
class RoleSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ('*', 'user', 'team', 'project', 'inventory',
|
||||
'permission_type', 'run_ad_hoc_commands')
|
||||
model = Role
|
||||
fields = ('*',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(PermissionSerializer, self).get_related(obj)
|
||||
if obj.user and obj.user.is_active:
|
||||
res['user'] = reverse('api:user_detail', args=(obj.user.pk,))
|
||||
if obj.team and obj.team.active:
|
||||
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
|
||||
if obj.project and obj.project.active:
|
||||
res['project'] = reverse('api:project_detail', args=(obj.project.pk,))
|
||||
if obj.inventory and obj.inventory.active:
|
||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||
return res
|
||||
ret = super(RoleSerializer, self).get_related(obj)
|
||||
if obj.content_object:
|
||||
if type(obj.content_object) is Organization:
|
||||
ret['organization'] = reverse('api:organization_detail', args=(obj.object_id,))
|
||||
if type(obj.content_object) is Team:
|
||||
ret['team'] = reverse('api:team_detail', args=(obj.object_id,))
|
||||
if type(obj.content_object) is Project:
|
||||
ret['project'] = reverse('api:project_detail', args=(obj.object_id,))
|
||||
if type(obj.content_object) is Inventory:
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
|
||||
# FIXME: may want to make some fields filtered based on user accessing
|
||||
@ -1705,7 +1748,7 @@ class JobRelaunchSerializer(JobSerializer):
|
||||
obj = self.context.get('obj')
|
||||
data = self.context.get('data')
|
||||
|
||||
# Check for passwords needed
|
||||
# Check for passwords needed
|
||||
needed = self.get_passwords_needed_to_start(obj)
|
||||
provided = dict([(field, data.get(field, '')) for field in needed])
|
||||
if not all(provided.values()):
|
||||
@ -2292,7 +2335,7 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
|
||||
class FactVersionSerializer(BaseFactSerializer):
|
||||
related = serializers.SerializerMethodField('get_related')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = FactVersion
|
||||
fields = ('related', 'module', 'timestamp',)
|
||||
|
||||
@ -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]+)/projects/$', 'user_projects_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'),
|
||||
)
|
||||
|
||||
@ -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]+)/users/$', 'team_users_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'),
|
||||
)
|
||||
|
||||
@ -141,8 +141,22 @@ credential_urls = patterns('awx.api.views',
|
||||
# See also credentials resources on users/teams.
|
||||
)
|
||||
|
||||
permission_urls = patterns('awx.api.views',
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'permission_detail'),
|
||||
role_urls = patterns('awx.api.views',
|
||||
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',
|
||||
@ -249,7 +263,8 @@ v1_urls = patterns('awx.api.views',
|
||||
url(r'^inventory_updates/', include(inventory_update_urls)),
|
||||
url(r'^inventory_scripts/', include(inventory_script_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'^jobs/', include(job_urls)),
|
||||
url(r'^job_host_summaries/', include(job_host_summary_urls)),
|
||||
|
||||
194
awx/api/views.py
194
awx/api/views.py
@ -581,7 +581,7 @@ class AuthTokenView(APIView):
|
||||
except IndexError:
|
||||
token = AuthToken.objects.create(user=serializer.validated_data['user'],
|
||||
request_hash=request_hash)
|
||||
# Get user un-expired tokens that are not invalidated that are
|
||||
# Get user un-expired tokens that are not invalidated that are
|
||||
# over the configured limit.
|
||||
# Mark them as invalid and inform the user
|
||||
invalid_tokens = AuthToken.get_tokens_over_limit(serializer.validated_data['user'])
|
||||
@ -700,24 +700,29 @@ class TeamUsersList(SubListCreateAttachDetachAPIView):
|
||||
parent_model = Team
|
||||
relationship = 'users'
|
||||
|
||||
class TeamPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
class TeamRolesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Team
|
||||
relationship = 'permissions'
|
||||
parent_key = 'team'
|
||||
relationship='member_role.children'
|
||||
|
||||
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'])
|
||||
base = Permission.objects.filter(team = team)
|
||||
#if Team.can_user_administrate(self.request.user, team, None):
|
||||
if self.request.user.can_access(Team, 'change', team, None):
|
||||
return base
|
||||
elif team.users.filter(pk=self.request.user.pk).count() > 0:
|
||||
return base
|
||||
raise PermissionDenied()
|
||||
return team.member_role.children
|
||||
|
||||
# XXX: Need to enforce permissions
|
||||
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 TeamProjectsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
@ -920,13 +925,30 @@ class UserTeamsList(SubListAPIView):
|
||||
parent_model = User
|
||||
relationship = 'teams'
|
||||
|
||||
class UserPermissionsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
class UserRolesList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = User
|
||||
relationship = 'permissions'
|
||||
parent_key = 'user'
|
||||
relationship='roles'
|
||||
|
||||
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):
|
||||
|
||||
@ -1047,10 +1069,6 @@ class CredentialActivityStreamList(SubListAPIView):
|
||||
# Okay, let it through.
|
||||
return super(type(self), self).get(request, *args, **kwargs)
|
||||
|
||||
class PermissionDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = Permission
|
||||
serializer_class = PermissionSerializer
|
||||
|
||||
class InventoryScriptList(ListCreateAPIView):
|
||||
|
||||
@ -2872,7 +2890,7 @@ class UnifiedJobStdout(RetrieveAPIView):
|
||||
return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
|
||||
else:
|
||||
return Response(response_message)
|
||||
|
||||
|
||||
if request.accepted_renderer.format in ('html', 'api', 'json'):
|
||||
content_format = request.query_params.get('content_format', 'html')
|
||||
content_encoding = request.query_params.get('content_encoding', None)
|
||||
@ -3031,6 +3049,134 @@ class SettingsReset(APIView):
|
||||
TowerSettings.objects.filter(key=settings_key).delete()
|
||||
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
|
||||
# in URL patterns and reverse URL lookups, converting CamelCase names to
|
||||
# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view).
|
||||
|
||||
@ -1659,6 +1659,64 @@ class TowerSettingsAccess(BaseAccess):
|
||||
def can_delete(self, obj):
|
||||
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(Organization, OrganizationAccess)
|
||||
register_access(Inventory, InventoryAccess)
|
||||
@ -1685,3 +1743,5 @@ register_access(UnifiedJob, UnifiedJobAccess)
|
||||
register_access(ActivityStream, ActivityStreamAccess)
|
||||
register_access(CustomInventoryScript, CustomInventoryScriptAccess)
|
||||
register_access(TowerSettings, TowerSettingsAccess)
|
||||
register_access(Role, RoleAccess)
|
||||
register_access(Resource, ResourceAccess)
|
||||
|
||||
@ -18,6 +18,7 @@ from awx.main.models.activity_stream import * # noqa
|
||||
from awx.main.models.ha import * # noqa
|
||||
from awx.main.models.configuration 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
|
||||
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
||||
|
||||
@ -156,6 +156,35 @@ class Resource(CommonModelNameNotUnique):
|
||||
return {k[4:]:v for k,v in res[0].items()}
|
||||
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):
|
||||
'''
|
||||
|
||||
@ -338,7 +338,6 @@ model_serializer_mapping = {
|
||||
Credential: CredentialSerializer,
|
||||
Team: TeamSerializer,
|
||||
Project: ProjectSerializer,
|
||||
Permission: PermissionSerializer,
|
||||
JobTemplate: JobTemplateSerializer,
|
||||
Job: JobSerializer,
|
||||
AdHocCommand: AdHocCommandSerializer,
|
||||
|
||||
@ -2,9 +2,6 @@ import pytest
|
||||
|
||||
from django.core.urlresolvers import resolve
|
||||
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 rest_framework.test import (
|
||||
@ -25,6 +22,9 @@ from awx.main.models.organization import (
|
||||
Team,
|
||||
)
|
||||
|
||||
from awx.main.models.rbac import Role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
def u(name, is_superuser=False):
|
||||
@ -89,6 +89,22 @@ def credential():
|
||||
def inventory(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
|
||||
def group(inventory):
|
||||
def g(name):
|
||||
@ -108,6 +124,7 @@ def permissions():
|
||||
'update':False, 'delete':False, 'scm_update':False, 'execute':False, 'use':True,},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def post():
|
||||
def rf(url, data, user=None, middleware=None, **kwargs):
|
||||
|
||||
365
awx/main/tests/functional/test_rbac_api.py
Normal file
365
awx/main/tests/functional/test_rbac_api.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user